From 8d83a8f834e339769051122143af7d682f53eda3 Mon Sep 17 00:00:00 2001 From: Jared Hughes Date: Sat, 11 Nov 2023 04:24:12 -0800 Subject: [PATCH 01/10] Initial TeX support --- package.json | 1 + src/IR/IR.ts | 38 +++--- src/IR/exprs.ts | 27 ++++ src/IR/functions.ts | 54 ++++++++ src/IR/terminals.ts | 6 + src/IR/toplevel.ts | 16 +++ src/IR/types.ts | 5 +- src/common/Spine.ts | 23 +++- src/common/emit.ts | 6 +- src/common/getType.ts | 2 + src/common/stringify.ts | 2 +- src/common/symbols.ts | 5 + src/languages/languages.ts | 2 + src/languages/tex/detokenizer.ts | 49 +++++++ src/languages/tex/emit.ts | 125 +++++++++++++++++ src/languages/tex/index.ts | 46 +++++++ src/languages/tex/plugins.ts | 222 +++++++++++++++++++++++++++++++ src/plugins/idents.ts | 27 +++- src/plugins/loops.ts | 73 +++++++--- 19 files changed, 686 insertions(+), 43 deletions(-) create mode 100644 src/IR/functions.ts create mode 100644 src/languages/tex/detokenizer.ts create mode 100644 src/languages/tex/emit.ts create mode 100644 src/languages/tex/index.ts create mode 100644 src/languages/tex/plugins.ts diff --git a/package.json b/package.json index f0a58b38..2ba586d9 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "test:jest": "jest --config jest.config.js", "test": "npm run test:formatting && npm run test:typecheck && npm run test:lint && npm run test:build && npm run test:jest", "cli": "npm run build && node --enable-source-maps dist/cli.js", + "cli-debug": "npm run build && node --inspect-brk dist/cli.js", "test:build": "npm run build && node \"dist/markdown-tests/build.js\"" }, "repository": { diff --git a/src/IR/IR.ts b/src/IR/IR.ts index 04ce1bcc..7b512be2 100644 --- a/src/IR/IR.ts +++ b/src/IR/IR.ts @@ -9,22 +9,24 @@ import { type VarDeclarationBlock, } from "./assignments"; import { type Array, type List, type Table, type Set } from "./collections"; -import { - type Op, - type Infix, - type ConditionalOp, - type FunctionCall, - type MethodCall, - type Prefix, - type IndexCall, - type KeyValue, - type RangeIndexCall, - type Function, - type NamedArg, - type ImplicitConversion, - type PropertyCall, - type Postfix, +import type { + Op, + Infix, + ConditionalOp, + FunctionCall, + MethodCall, + Prefix, + IndexCall, + KeyValue, + RangeIndexCall, + Function, + NamedArg, + ImplicitConversion, + PropertyCall, + Postfix, + ScanningMacroCall, } from "./exprs"; +import type { FunctionDefinition } from "./functions"; import { type ForRange, type ForEach, @@ -41,7 +43,7 @@ import { type Integer, type Text, } from "./terminals"; -import { type Block, type If, type Import, type Variants } from "./toplevel"; +import type { CapturingBlock, Block, If, Import, Variants } from "./toplevel"; import { type Type } from "./types"; export * from "./assignments"; @@ -52,6 +54,7 @@ export * from "./loops"; export * from "./terminals"; export * from "./toplevel"; export * from "./types"; +export * from "./functions"; export interface BaseNode { readonly source?: SourcePointer; @@ -90,6 +93,8 @@ export type Node = | ForArgv | If // Other nodes + | FunctionDefinition + | CapturingBlock | ImplicitConversion | VarDeclaration | VarDeclarationWithAssignment @@ -99,6 +104,7 @@ export type Node = | MutatingInfix | IndexCall | RangeIndexCall + | ScanningMacroCall | MethodCall | PropertyCall | Infix diff --git a/src/IR/exprs.ts b/src/IR/exprs.ts index e489b09e..0217235c 100644 --- a/src/IR/exprs.ts +++ b/src/IR/exprs.ts @@ -23,6 +23,8 @@ import { isBinary, booleanNotOpCode, type Text, + type IDCastable, + castID, } from "./IR"; export interface ImplicitConversion extends BaseNode { @@ -66,6 +68,20 @@ export interface FunctionCall extends BaseNode { readonly args: readonly Node[]; } +/** + * ScanningMacroCall is currently necessary to represent (TeX) + * \newcount\x \x123 + * since a regular FunctionCall would require curly braces since 123 is three tokens. + * \def\f#1{(#1)} \f{123} + * This is a correctness issue (not just golfing) since curly braces does not work for counter: + * \newcount\x \x{123} % Missing number, treated as zero. + */ +export interface ScanningMacroCall extends BaseNode { + readonly kind: "ScanningMacroCall"; + readonly func: Node; + readonly args: readonly Node[]; +} + export interface MethodCall extends BaseNode { readonly kind: "MethodCall"; readonly object: Node; @@ -335,6 +351,17 @@ export function functionCall( }; } +export function scanningMacroCall( + func: IDCastable, + ...args: readonly Node[] +): ScanningMacroCall { + return { + kind: "ScanningMacroCall", + func: castID(func), + args, + }; +} + export function methodCall( object: Node, ident: string | Identifier, diff --git a/src/IR/functions.ts b/src/IR/functions.ts new file mode 100644 index 00000000..5b535266 --- /dev/null +++ b/src/IR/functions.ts @@ -0,0 +1,54 @@ +import { + type Identifier, + type BaseNode, + type Node, + type IDCastable, + castID, +} from "./IR"; + +// This file is for functions and macros. + +/** + * FunctionDefinition is currently only used for TeX, where the identifiers are + * emitted as macro symbols: + * + * \def\f#1#2{(#1,#2,#1,#2)} + * + * While TeX calls these "macros", they happen at runtime. + * Macros (like #define from C) are compile-time. + * + * Note this currently assumes the macros don't do any scanning. + * E.g. TeX supports + * \def\f#1,#2;{#1#2#1#2} \f123,456; + * in lieu of + * \def\f#1#2{#1#2#1#2} \f{123}{456} + * For now, we don't allow the former. Allowing it will need a different node type. + */ +export interface FunctionDefinition extends BaseNode { + readonly kind: "FunctionDefinition"; + readonly name: Identifier; + readonly args: readonly Identifier[]; + readonly body: Node; + /** Does the definition apply to all parent scopes too? + * In TeX: false = \def or \edef; true = \gdef or \xdef */ + readonly isGlobal: boolean; + /** Does the definition expand its argument before defining? + * In TeX: false = \def or \gdef; true = \edef or \xdef */ + readonly isExpanded: boolean; +} + +export function functionDefinition( + name: IDCastable, + args: readonly IDCastable[], + body: Node, + opts: { isGlobal?: boolean; isExpanded?: boolean } = {}, +): FunctionDefinition { + return { + kind: "FunctionDefinition", + name: castID(name), + args: args.map(castID), + body, + isGlobal: opts.isGlobal ?? false, + isExpanded: opts.isExpanded ?? false, + }; +} diff --git a/src/IR/terminals.ts b/src/IR/terminals.ts index bf71b089..d661e43c 100644 --- a/src/IR/terminals.ts +++ b/src/IR/terminals.ts @@ -45,6 +45,12 @@ export interface Text extends BaseNode { readonly value: Value; } +export type IDCastable = string | Identifier; +export function castID(name: IDCastable) { + if (typeof name === "string") return id(name); + return name; +} + export function id(name: string, builtin: boolean = false): Identifier { return { kind: "Identifier", name, builtin }; } diff --git a/src/IR/toplevel.ts b/src/IR/toplevel.ts index f72f6b6e..763243cb 100644 --- a/src/IR/toplevel.ts +++ b/src/IR/toplevel.ts @@ -16,6 +16,22 @@ export interface Block extends BaseNode { readonly children: readonly Node[]; } +/** + * A block of several statements, capturing all global variables. + * Any globals get reset when exiting the block. This is for TeX. + * + * In TeX, curly braces can be used for several purposes: + * - `\def\f#1#2{#1#2#1#2}`: definining a macro is not a capturing block. + * - `\f{abc}{def}`: grouping arguments does not create capturing blocks. + * - `{\advance\x\1\the\x}: isolated curly braces creates a capturing block. + * - `\def\f#1{{\advance\x\1#1}}`: more curly braces makes a capturing block. + */ +// TODO: remove CapturingBlock. It's currently unused. +export interface CapturingBlock extends BaseNode { + readonly kind: "CapturingBlock"; + readonly child: Node; +} + /** * A C-like if statement (not ternary expression). Raw OK * diff --git a/src/IR/types.ts b/src/IR/types.ts index 54b59993..03279c34 100644 --- a/src/IR/types.ts +++ b/src/IR/types.ts @@ -65,9 +65,10 @@ export const int53Type: Type = integerType( -9007199254740992n, 9007199254740991n, ); +export const int32Type: Type = integerType(-2147483648n, 2147483647n); export function type( - type: Type | "void" | "boolean" | "int64" | "int53", + type: Type | "void" | "boolean" | "int64" | "int53" | "int32", ): Type { switch (type) { case "void": @@ -78,6 +79,8 @@ export function type( return int64Type; case "int53": return int53Type; + case "int32": + return int32Type; default: return type; } diff --git a/src/common/Spine.ts b/src/common/Spine.ts index 19b50d3c..8c49f827 100644 --- a/src/common/Spine.ts +++ b/src/common/Spine.ts @@ -1,4 +1,4 @@ -import { type IR, isOp, op } from "../IR"; +import { type IR, isOp, op, block } from "../IR"; import { type CompilationContext } from "./compile"; import { getChild, getChildFragments, type PathFragment } from "./fragments"; import { replaceAtIndex } from "./arrays"; @@ -176,6 +176,27 @@ export class Spine { return this.replacedWith(ret).withReplacer(replacer, skipReplaced, true); } } + + flatMapWithChildrenReplacer( + replacer: Visitor, + ): IR.Node | undefined { + if (this.node.kind !== "Block") return; + const children = this.node.children; + let newChildren: IR.Node[] | undefined; + for (let i = 0; i < children.length; i++) { + const child = this.getChild({ prop: "children", index: i }); + const replacement = replacer(child.node, child); + if (replacement !== undefined) { + if (newChildren === undefined) { + newChildren = children.slice(0, i); + } + newChildren.push(...replacement); + } else if (newChildren !== undefined) { + newChildren.push(child.node); + } + } + if (newChildren !== undefined) return block(newChildren); + } } export type PluginVisitor = ( diff --git a/src/common/emit.ts b/src/common/emit.ts index 21a34a76..f280eb6c 100644 --- a/src/common/emit.ts +++ b/src/common/emit.ts @@ -59,10 +59,10 @@ export function containsMultiNode(exprs: readonly IR.Node[]): boolean { export class EmitError extends PolygolfError { constructor(expr: Node, detail?: string) { - if (detail === undefined && "op" in expr && expr.op !== null) - detail = expr.op; + const kind = + expr.kind + ("op" in expr && expr.op !== null ? `[${expr.op}]` : ""); detail = detail === undefined ? "" : ` (${detail})`; - const message = `emit error - ${expr.kind}${detail} not supported.`; + const message = `emit error - ${kind}${detail} not supported.`; super(message, expr.source); this.name = "EmitError"; Object.setPrototypeOf(this, EmitError.prototype); diff --git a/src/common/getType.ts b/src/common/getType.ts index 12b1ce76..73efad84 100644 --- a/src/common/getType.ts +++ b/src/common/getType.ts @@ -79,6 +79,8 @@ export function calcType(expr: Node, program: Node): Type { switch (expr.kind) { case "Function": return functionType(expr.args.map(type), type(expr.expr)); + case "FunctionDefinition": + return functionType(expr.args.map(type), type(expr.body)); case "Block": case "VarDeclaration": return voidType; diff --git a/src/common/stringify.ts b/src/common/stringify.ts index 5415275c..2af221be 100644 --- a/src/common/stringify.ts +++ b/src/common/stringify.ts @@ -6,7 +6,7 @@ export function stringify(x: Node, skipTargetType = false): string { const result = JSON.stringify( x, (key, value) => - key === "source" + key === "source" || key === "type" ? undefined : key === "targetType" && skipTargetType ? undefined diff --git a/src/common/symbols.ts b/src/common/symbols.ts index 2a450fe2..328284ac 100644 --- a/src/common/symbols.ts +++ b/src/common/symbols.ts @@ -116,6 +116,11 @@ function introducedSymbols( .filter(isIdent()) .filter((x) => !existing.has(x.name)) .map((x) => x.name); + // TODO: I may have broken some other languages with VarDeclaration change. + case "VarDeclaration": + return [node.variable.name]; + case "FunctionDefinition": + return [node.name.name]; } } diff --git a/src/languages/languages.ts b/src/languages/languages.ts index 572e5edd..4d702e88 100644 --- a/src/languages/languages.ts +++ b/src/languages/languages.ts @@ -6,6 +6,7 @@ import pythonLanguage from "./python"; import swiftLanguage from "./swift"; import golfscriptLanguage from "./golfscript"; import javascriptLanguage from "./javascript"; +import texLanguage from "./tex"; const languages = [ golfscriptLanguage, @@ -15,6 +16,7 @@ const languages = [ swiftLanguage, polygolfLanguage, javascriptLanguage, + texLanguage, ]; export default languages; diff --git a/src/languages/tex/detokenizer.ts b/src/languages/tex/detokenizer.ts new file mode 100644 index 00000000..f1f88916 --- /dev/null +++ b/src/languages/tex/detokenizer.ts @@ -0,0 +1,49 @@ +import { flattenTree, type TokenTree } from "../../common/Language"; + +// Special tokens: +// SPACE_TYPOGRAPHY: converts to " ", or +// "\\ " if it's preceded by a control word like \f or another SPACE_TYPOGRAPHY +// SPACE_ANTIGOBBLE: converts to " ", or "{}" if it's followed by a SPACE_TYPOGRAPHY +// Intended to be placed after digits which are parts of numbers +// Without the anti-gobble, TeX would keep trying to gobble tokens until it +// reaches a non-digit. This includes expanding macros, so +// `\newcount\x \def\f{\advance\x1 } \x5 \f \the\x,\the\x` prints '6,6', but +// `\newcount\x \def\f{\advance\x1} \x5 \f \the\x` prints ',20' (since 20=5+15) +// TODO-tex-improvement: anti-gobble spaces can often be removed, e.g. before an \advance. +// Requires control flow analysis to do perfectly. + +export const SPACE_TYPOGRAPHY = "$SPACE_TYPOGRAPHY$"; +export const SPACE_ANTIGOBBLE = "$SPACE_ANTIGOBBLE$"; + +const controlWordRegex = /^\\[a-zA-Z]+$/; + +export function texDetokenizer(tree: TokenTree): string { + const tokens: string[] = flattenTree(tree); + let result = tokens[0]; + for (let i = 1; i < tokens.length; i++) { + result += token(tokens, i); + } + return result; +} + +function token(tokens: string[], i: number) { + switch (tokens[i]) { + case SPACE_TYPOGRAPHY: + if ( + controlWordRegex.test(tokens[i - 1]) || + tokens[i - 1] === SPACE_TYPOGRAPHY + ) { + return "\\ "; + } else { + return " "; + } + case SPACE_ANTIGOBBLE: + if (i < tokens.length - 1 && tokens[i + 1] === SPACE_TYPOGRAPHY) { + return "{}"; + } else { + return " "; + } + default: + return tokens[i]; + } +} diff --git a/src/languages/tex/emit.ts b/src/languages/tex/emit.ts new file mode 100644 index 00000000..7b0a5a0b --- /dev/null +++ b/src/languages/tex/emit.ts @@ -0,0 +1,125 @@ +import { type TokenTree } from "@/common/Language"; +import { EmitError, emitIntLiteral } from "../../common/emit"; +import { type IR } from "../../IR"; +import { type CompilationContext } from "@/common/compile"; +import { SPACE_ANTIGOBBLE } from "./detokenizer"; + +// TODO-tex: somehow deal with quoting text, e.g. '#' cannot be written as-is. +// TODO-tex: <=, >=,!= are unsupported. Plugin to convert to >,<,= +// TODO-tex: would counter/helper defs go in imports.ts? + +export default function emitProgram( + program: IR.Node, + context: CompilationContext, +): TokenTree { + return new TexEmitter(program, context).emitProgram(); +} + +const macroParamRegex = /^#[1-9]$/; + +interface EmitContext { + /** + * `scanningFor` is currently not used. I introduced it because I forgot you're + * allowed to nest `\if`s. https://tex.stackexchange.com/a/315757/288147. + * It will be useful as a check for nesting scanning macros. + */ + readonly scanningFor: readonly string[]; + readonly macroDepth: number; +} + +class TexEmitter { + constructor( + public program: IR.Node, + public ctx: CompilationContext, + ) {} + + emitProgram() { + return this.emit(this.program); + } + + private emitContext: EmitContext = { + scanningFor: [], + macroDepth: 0, + }; + + private readonly emitContextStack: EmitContext[] = []; + + private pushScanningFor(s: string) { + this.pushContext({ + scanningFor: [...this.emitContext.scanningFor, s], + }); + } + + private pushContext(c: Partial) { + this.emitContextStack.push(this.emitContext); + this.emitContext = { ...this.emitContext, ...c }; + } + + private popContext() { + const c = this.emitContextStack.pop(); + if (c === undefined) throw new Error("Popped more contexts than pushed"); + return this.emitContext; + } + + private emit(e: IR.Node, withContext?: EmitContext): TokenTree { + if (withContext === undefined) return this._emit(e); + const currContext = this.emitContext; + this.emitContext = withContext; + const ret = this._emit(e); + this.emitContext = currContext; + return ret; + } + + private emitInsideCurlies(n: IR.Node): TokenTree { + this.pushContext({ scanningFor: [] }); + const ret = ["{", this.emit(n), "}"]; + this.popContext(); + return ret; + } + + private _emit(e: IR.Node): TokenTree { + const emit = (n: IR.Node) => this._emit(n); + switch (e.kind) { + case "Block": + return e.children.map(emit); + case "CapturingBlock": + return this.emitInsideCurlies(e); + case "FunctionDefinition": + return this.emitDef(e); + case "ScanningMacroCall": + return [emit(e.func), e.args.map(emit)]; + case "FunctionCall": + return [emit(e.func), e.args.map((a) => this.emitInsideCurlies(a))]; + case "Identifier": + return e.name; + case "Integer": + return [emitIntLiteral(e), SPACE_ANTIGOBBLE]; + case "VarDeclaration": + return ["\\newcount", e.variable.name]; + case "Text": + // TODO: deal with escapes. + return e.value; + default: + throw new EmitError(e); + } + } + + private emitDef(e: IR.FunctionDefinition): TokenTree { + const ids = e.args.map((id) => { + if (!macroParamRegex.test(id.name)) + throw new EmitError(id, "Invalid macro parameter."); + const depth = this.emitContext.macroDepth; + if (depth >= 3) throw new Error("Macro definitions nested too far"); + return "#".repeat(2 ** depth) + id.name[1]; + }); + const slashDef = e.isGlobal + ? e.isExpanded + ? "\\xdef" + : "\\gdef" + : e.isExpanded + ? "\\edef" + : "\\def"; + const body = this.emitInsideCurlies(e.body); + return [slashDef, e.name.name, ids, "{", body, "}"]; + } +} diff --git a/src/languages/tex/index.ts b/src/languages/tex/index.ts new file mode 100644 index 00000000..a8e39cbb --- /dev/null +++ b/src/languages/tex/index.ts @@ -0,0 +1,46 @@ +import emitProgram from "./emit"; +import { mapToPrefixAndInfix } from "../../plugins/ops"; +import { forRangeToWhile, whileToRecursion } from "../../plugins/loops"; +import { lettersOnlyIdentGen, renameIdents } from "../../plugins/idents"; +import { type Language, required } from "../../common/Language"; +import { + stuffToMacros, + insertAccumulatedCounters, + exprTreeToFlat2AC, +} from "./plugins"; +import { texDetokenizer } from "./detokenizer"; + +const texLanguage: Language = { + name: "TeX", + extension: "tex", + emitter: emitProgram, + detokenizer: texDetokenizer, + phases: [ + required( + forRangeToWhile, + whileToRecursion, + exprTreeToFlat2AC, + mapToPrefixAndInfix( + { + // TODO: I don't think neg works currently + neg: "-", + mul: "\\multiply", + // TODO: check if TeX is trunc div or floor div. + div: "\\divide", + add: "\\advance", + // TODO: sub works with \\advance- + }, + true, + ), + stuffToMacros, + insertAccumulatedCounters, + renameIdents({ + preferred: (o) => lettersOnlyIdentGen.preferred(o).map((w) => "\\" + w), + short: ["~"].concat(lettersOnlyIdentGen.short.map((c) => "\\" + c)), + general: (i) => "\\" + lettersOnlyIdentGen.general(i), + }), + ), + ], +}; + +export default texLanguage; diff --git a/src/languages/tex/plugins.ts b/src/languages/tex/plugins.ts new file mode 100644 index 00000000..8a029c5c --- /dev/null +++ b/src/languages/tex/plugins.ts @@ -0,0 +1,222 @@ +import type { Plugin } from "@/common/Language"; +import { + functionDefinition, + int32Type, + isSubtype, + type IR, + scanningMacroCall, + textType, + integerType, + text, + id, + varDeclaration, + block, + type VarDeclaration, + type Node, + voidType, + type MutatingInfix, + assignment, + op, +} from "../../IR"; +import { getType } from "../../common/getType"; +import { EmitError } from "../../common/emit"; +import { type Spine } from "../../common/Spine"; + +type Immediate = IR.Identifier | IR.Integer; + +// true = isAscii +const texStringType = textType(integerType(0, "oo"), true); + +// TODO: I don't know what's the actual term. 3 argument code? +export const exprTreeToFlat2AC: Plugin = { + name: "exprTreeToFlat2AC", + visit(_node, spine) { + return spine.flatMapWithChildrenReplacer(exprTreeToFlat2ACVisitor); + }, +}; + +let globalID = 0; +function exprTreeToFlat2ACVisitor(node: IR.Node, spine: Spine) { + if (node.kind !== "Assignment") return; + if (node.variable.kind !== "Identifier") return; + // if (!isSubtype(getType(node, spine), int32Type)) return; + const treeID = ++globalID; + function ipID(ip: number) { + return id(`__tmp_ip_${treeID}_${ip}`); + } + const flat: IR.Assignment[] = []; + function rec(n: IR.Node): Immediate { + if (n.kind === "Integer") return n; + if (n.kind === "Identifier") return n; + if (n.kind !== "Op") throw new EmitError(n, "tree has a not-Op"); + if (n.args.length !== 2) throw new EmitError(n, "not two args"); + // left + const left = rec(n.args[0]); + const newVar = ipID(flat.length); + const node = assignment(newVar, left); + flat.push(node); + // right, and compute. + const right = rec(n.args[1]); + const opres = op(n.op, newVar, right); + flat.push(assignment(newVar, opres)); + return newVar; + } + const res = rec(node.expr); + flat.push(assignment(node.variable, res)); + return flat; +} + +/** Bad global state. Insert strings that will need to be counter names. */ +const accumulatedCounters = new Set(); + +export const stuffToMacros: Plugin = { + name: "stuffToMacros", + visit(node, spine) { + switch (node.kind) { + case "Assignment": + return assignmentToMacros(node, spine); + case "MutatingInfix": + return mutatingInfixToMacros(node, spine); + case "If": + return ifToMacros(node, spine); + case "Op": + switch (node.op) { + case "println_int": { + const arg = node.args[0]; + assertImmediate(arg, "println_int"); + return voidIt( + // TODO: \\endgraf is long but works everywhere. Try \n\n sometimes + // TODO: the \\endgraf should be outside the other scanningMacroCall. + // Works out the same for emit, just feels wrong. + scanningMacroCall( + id("\\the", true), + arg, + scanningMacroCall(id("\\endgraf", true)), + ), + ); + } + } + } + }, +}; + +function assignmentToMacros( + node: IR.Assignment, + spine: Spine, +): IR.Node | undefined { + const { variable, expr } = node; + const varType = getType(variable, spine.root); + const exprType = getType(expr, spine.root); + + if (isSubtype(varType, int32Type) && isSubtype(exprType, int32Type)) { + // variable is a counter + // TODO-tex: ensure variable is \newcount'd. + assertIdentifier(variable, "in counter assignment LHS"); + assertImmediate(expr, "in counter assignment RHS"); + accumulatedCounters.add(variable.name); + return voidIt(scanningMacroCall(variable, expr)); + } else if ( + isSubtype(varType, texStringType) && + isSubtype(exprType, texStringType) + ) { + assertIdentifier(variable, "in string assignment LHS"); + // TODO: the expr cannot be a macro that edits any counters, since that's + // not allowed inside an edef. + return functionDefinition(variable, [], expr, { + isGlobal: true, + isExpanded: true, + }); + } else { + throw new EmitError(node, "not integer and not string"); + } +} + +function mutatingInfixToMacros(node: MutatingInfix, spine: Spine) { + const { variable, right } = node; + const varType = getType(variable, spine.root); + const rightType = getType(right, spine.root); + + if (isSubtype(varType, int32Type) && isSubtype(rightType, int32Type)) { + // variable is a counter + // TODO-tex: ensure variable is \newcount'd. + assertIdentifier(variable, "in counter assignment LHS"); + assertImmediate(right, "in counter assignment RHS"); + // TODO: negative: + // case "-": + // if (right.kind === "Integer" && right.value < 0) { + // const neg = { ...right, value: -right.value }; + // return ["\\advance", variable.name, this.emit(neg)]; + // } + // return ["\\advance", variable.name, "-", this.emit(right)]; + accumulatedCounters.add(variable.name); + const macroName = id(node.name, true); + return voidIt(scanningMacroCall(macroName, variable, right)); + } else { + throw new EmitError(node, "not integer"); + } +} + +function ifToMacros(node: IR.Node, spine: Spine) { + if (node.kind !== "If") return; + const cond = node.condition; + if (cond.kind !== "Op" || cond.args.length !== 2) + throw new EmitError(cond, "inside if"); + const [left, right] = cond.args; + const leftType = getType(left, spine.root); + const rightType = getType(right, spine.root); + if (isSubtype(leftType, int32Type) && isSubtype(rightType, int32Type)) { + const op = { gt: ">", lt: "<", eq: "=" }[cond.op as string]; + if (op === undefined) throw new EmitError(cond); + assertImmediate(left, "inside \\ifnum"); + assertImmediate(right, "inside \\ifnum"); + const n = scanningMacroCall( + id("\\ifnum", true), + left, + text(op), + right, + node.consequent, + ...(node.alternate !== undefined + ? [id("\\else", true), node.alternate] + : []), + id("\\fi", true), + ); + return voidIt(n); + } else { + throw new EmitError(node, "comparing non-integers"); + } +} + +export const insertAccumulatedCounters: Plugin = { + name: "insertAccumulatedCounters", + visit(node, spine) { + if (!spine.isRoot) return; + const decls: VarDeclaration[] = []; + for (const name of accumulatedCounters) { + decls.push(varDeclaration(name, int32Type)); + } + // Clear accumulatedCounters for the next emit pass. + accumulatedCounters.clear(); + if (decls.length > 0) { + const children: Node[] = decls; + children.push(node); + return block(children); + } + return undefined; + }, +}; + +function voidIt(n: Node) { + return { ...n, type: voidType }; +} + +function assertIdentifier( + n: IR.Node, + detail: string, +): asserts n is IR.Identifier { + if (n.kind !== "Identifier") throw new EmitError(n, detail); +} + +function assertImmediate(n: IR.Node, detail: string): asserts n is Immediate { + if (n.kind !== "Identifier" && n.kind !== "Integer") + throw new EmitError(n, detail); +} diff --git a/src/plugins/idents.ts b/src/plugins/idents.ts index 34fa1f38..7984ebe6 100644 --- a/src/plugins/idents.ts +++ b/src/plugins/idents.ts @@ -1,5 +1,5 @@ import { type Plugin, type IdentifierGenerator } from "common/Language"; -import { getDeclaredIdentifiers } from "../common/symbols"; +import { getDeclaredIdentifiers, symbolTableRoot } from "../common/symbols"; import { type Spine } from "../common/Spine"; import { assignment, @@ -84,6 +84,8 @@ export function renameIdents( }; } +const letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"; + const defaultIdentGen: IdentifierGenerator = { preferred(original: string) { const firstLetter = [...original].find((x) => /[A-Za-z]/.test(x)); @@ -92,10 +94,31 @@ const defaultIdentGen: IdentifierGenerator = { const upper = firstLetter.toUpperCase(); return [firstLetter, firstLetter === lower ? upper : lower]; }, - short: "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ".split(""), + short: letters.split(""), general: (i) => `v${i}`, }; +export const lettersOnlyIdentGen: IdentifierGenerator = { + ...defaultIdentGen, + general: (i) => { + let s = ""; + // 0 is the first general id. Skip over the shorts, so map it to 53. + i += 53; + while (i > 0) { + // Base 52, but with digits 1 to 52 instead of 0 to 51 + const r = i % 52; + if (r === 0) { + s += "z"; + i = Math.floor(i / 52) - 1; + } else { + s += letters[r - 1]; + i = Math.floor(i / 52); + } + } + return s; + }, +}; + /** * Aliases repeated expressions by mapping them to new variables. * @param getKey Calculates a key to compare expressions, `undefined` marks aliasing should not happen. diff --git a/src/plugins/loops.ts b/src/plugins/loops.ts index 03482307..8afef3f4 100644 --- a/src/plugins/loops.ts +++ b/src/plugins/loops.ts @@ -29,6 +29,9 @@ import { isText, isIdent, isUserIdent, + functionDefinition, + ifStatement, + functionCall, } from "../IR"; import { byteLength, charLength } from "../common/objective"; import { PolygolfError } from "../common/errors"; @@ -56,28 +59,32 @@ export function forRangeToForRangeInclusive(skip1Step = false): Plugin { export const forRangeToWhile: Plugin = { name: "forRangeToWhile", - visit(node, spine) { - if (node.kind === "ForRange" && node.variable !== undefined) { - const low = getType(node.start, spine); - const high = getType(node.end, spine); - if (low.kind !== "integer" || high.kind !== "integer") { - throw new Error(`Unexpected type (${low.kind},${high.kind})`); - } - const increment = assignment( - node.variable, - op("add", node.variable, node.increment), - ); - return block([ - assignment(node.variable, node.start), - whileLoop( - op(node.inclusive ? "leq" : "lt", node.variable, node.end), - block([node.body, increment]), - ), - ]); - } + visit(_node, spine) { + return spine.flatMapWithChildrenReplacer(forRangeToWhileVisitor); }, }; +function forRangeToWhileVisitor(node: IR.Node, spine: Spine) { + if (node.kind === "ForRange" && node.variable !== undefined) { + const low = getType(node.start, spine); + const high = getType(node.end, spine); + if (low.kind !== "integer" || high.kind !== "integer") { + throw new Error(`Unexpected type (${low.kind},${high.kind})`); + } + const increment = assignment( + node.variable, + op("add", node.variable, node.increment), + ); + return [ + assignment(node.variable, node.start), + whileLoop( + op(node.inclusive ? "leq" : "lt", node.variable, node.end), + block([node.body, increment]), + ), + ]; + } +} + export const forRangeToForCLike: Plugin = { name: "forRangeToForCLike", visit(node, spine) { @@ -437,3 +444,31 @@ export const removeUnusedForVar: Plugin = { } }, }; + +// TODO: global counter here silly; +let globalID = 0; +function tempId() { + return `__tmp_id_${globalID++}`; +} + +export const whileToRecursion: Plugin = { + name: "whileToRecursion", + visit(_node, spine) { + return spine.flatMapWithChildrenReplacer(whileToRecursionVisitor); + }, +}; + +function whileToRecursionVisitor(node: IR.Node) { + if (node.kind !== "While") return; + const name = id(tempId()); + return [ + // Create a function to perform the while + functionDefinition( + name, + [], + ifStatement(node.condition, block([node.body, functionCall(name)])), + ), + // Call that function + functionCall(name), + ]; +} From 349f49b96acaaa56d7cfa993b8f93b3e6b755af2 Mon Sep 17 00:00:00 2001 From: Jared Hughes Date: Sat, 11 Nov 2023 13:26:18 -0800 Subject: [PATCH 02/10] Cleanups from review --- src/common/emit.ts | 3 +-- src/common/stringify.ts | 2 +- src/languages/tex/emit.ts | 2 +- src/languages/tex/plugins.ts | 17 +++++++++-------- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/common/emit.ts b/src/common/emit.ts index f280eb6c..94d05ba7 100644 --- a/src/common/emit.ts +++ b/src/common/emit.ts @@ -59,8 +59,7 @@ export function containsMultiNode(exprs: readonly IR.Node[]): boolean { export class EmitError extends PolygolfError { constructor(expr: Node, detail?: string) { - const kind = - expr.kind + ("op" in expr && expr.op !== null ? `[${expr.op}]` : ""); + const kind = expr.kind + ("op" in expr ? `[${expr.op}]` : ""); detail = detail === undefined ? "" : ` (${detail})`; const message = `emit error - ${kind}${detail} not supported.`; super(message, expr.source); diff --git a/src/common/stringify.ts b/src/common/stringify.ts index 2af221be..5415275c 100644 --- a/src/common/stringify.ts +++ b/src/common/stringify.ts @@ -6,7 +6,7 @@ export function stringify(x: Node, skipTargetType = false): string { const result = JSON.stringify( x, (key, value) => - key === "source" || key === "type" + key === "source" ? undefined : key === "targetType" && skipTargetType ? undefined diff --git a/src/languages/tex/emit.ts b/src/languages/tex/emit.ts index 7b0a5a0b..90f2f483 100644 --- a/src/languages/tex/emit.ts +++ b/src/languages/tex/emit.ts @@ -120,6 +120,6 @@ class TexEmitter { ? "\\edef" : "\\def"; const body = this.emitInsideCurlies(e.body); - return [slashDef, e.name.name, ids, "{", body, "}"]; + return [slashDef, e.name.name, ids, body]; } } diff --git a/src/languages/tex/plugins.ts b/src/languages/tex/plugins.ts index 8a029c5c..fbc5299e 100644 --- a/src/languages/tex/plugins.ts +++ b/src/languages/tex/plugins.ts @@ -17,6 +17,7 @@ import { type MutatingInfix, assignment, op, + isOfKind, } from "../../IR"; import { getType } from "../../common/getType"; import { EmitError } from "../../common/emit"; @@ -105,8 +106,8 @@ function assignmentToMacros( spine: Spine, ): IR.Node | undefined { const { variable, expr } = node; - const varType = getType(variable, spine.root); - const exprType = getType(expr, spine.root); + const varType = getType(variable, spine); + const exprType = getType(expr, spine); if (isSubtype(varType, int32Type) && isSubtype(exprType, int32Type)) { // variable is a counter @@ -133,8 +134,8 @@ function assignmentToMacros( function mutatingInfixToMacros(node: MutatingInfix, spine: Spine) { const { variable, right } = node; - const varType = getType(variable, spine.root); - const rightType = getType(right, spine.root); + const varType = getType(variable, spine); + const rightType = getType(right, spine); if (isSubtype(varType, int32Type) && isSubtype(rightType, int32Type)) { // variable is a counter @@ -162,8 +163,8 @@ function ifToMacros(node: IR.Node, spine: Spine) { if (cond.kind !== "Op" || cond.args.length !== 2) throw new EmitError(cond, "inside if"); const [left, right] = cond.args; - const leftType = getType(left, spine.root); - const rightType = getType(right, spine.root); + const leftType = getType(left, spine); + const rightType = getType(right, spine); if (isSubtype(leftType, int32Type) && isSubtype(rightType, int32Type)) { const op = { gt: ">", lt: "<", eq: "=" }[cond.op as string]; if (op === undefined) throw new EmitError(cond); @@ -216,7 +217,7 @@ function assertIdentifier( if (n.kind !== "Identifier") throw new EmitError(n, detail); } +const isImmediate = isOfKind("Identifier", "Integer"); function assertImmediate(n: IR.Node, detail: string): asserts n is Immediate { - if (n.kind !== "Identifier" && n.kind !== "Integer") - throw new EmitError(n, detail); + if (!isImmediate(n)) throw new EmitError(n, detail); } From 95e6a1dfc83164da7f71ede255542abb594ef547 Mon Sep 17 00:00:00 2001 From: Jared Hughes Date: Sat, 11 Nov 2023 13:55:59 -0800 Subject: [PATCH 03/10] Add emit/parse for FunctionDefinition and ScanningMacroCall --- src/IR/IR.ts | 3 +-- src/IR/functions.ts | 6 +++++- src/IR/toplevel.ts | 16 ---------------- src/frontend/parse-emit.test.ts | 6 ++++++ src/frontend/parse.ts | 25 ++++++++++++++++++++++++- src/languages/polygolf/emit.ts | 9 +++++++++ src/languages/tex/emit.ts | 2 -- src/plugins/idents.ts | 2 +- 8 files changed, 46 insertions(+), 23 deletions(-) diff --git a/src/IR/IR.ts b/src/IR/IR.ts index 7b512be2..09822ed2 100644 --- a/src/IR/IR.ts +++ b/src/IR/IR.ts @@ -43,7 +43,7 @@ import { type Integer, type Text, } from "./terminals"; -import type { CapturingBlock, Block, If, Import, Variants } from "./toplevel"; +import type { Block, If, Import, Variants } from "./toplevel"; import { type Type } from "./types"; export * from "./assignments"; @@ -94,7 +94,6 @@ export type Node = | If // Other nodes | FunctionDefinition - | CapturingBlock | ImplicitConversion | VarDeclaration | VarDeclarationWithAssignment diff --git a/src/IR/functions.ts b/src/IR/functions.ts index 5b535266..f5cf6a18 100644 --- a/src/IR/functions.ts +++ b/src/IR/functions.ts @@ -43,12 +43,16 @@ export function functionDefinition( body: Node, opts: { isGlobal?: boolean; isExpanded?: boolean } = {}, ): FunctionDefinition { + const isExpanded = opts.isExpanded ?? false; + if (isExpanded && args.length > 0) { + throw new Error("Expanded definition cannot have any args."); + } return { kind: "FunctionDefinition", name: castID(name), args: args.map(castID), body, isGlobal: opts.isGlobal ?? false, - isExpanded: opts.isExpanded ?? false, + isExpanded, }; } diff --git a/src/IR/toplevel.ts b/src/IR/toplevel.ts index 763243cb..f72f6b6e 100644 --- a/src/IR/toplevel.ts +++ b/src/IR/toplevel.ts @@ -16,22 +16,6 @@ export interface Block extends BaseNode { readonly children: readonly Node[]; } -/** - * A block of several statements, capturing all global variables. - * Any globals get reset when exiting the block. This is for TeX. - * - * In TeX, curly braces can be used for several purposes: - * - `\def\f#1#2{#1#2#1#2}`: definining a macro is not a capturing block. - * - `\f{abc}{def}`: grouping arguments does not create capturing blocks. - * - `{\advance\x\1\the\x}: isolated curly braces creates a capturing block. - * - `\def\f#1{{\advance\x\1#1}}`: more curly braces makes a capturing block. - */ -// TODO: remove CapturingBlock. It's currently unused. -export interface CapturingBlock extends BaseNode { - readonly kind: "CapturingBlock"; - readonly child: Node; -} - /** * A C-like if statement (not ternary expression). Raw OK * diff --git a/src/frontend/parse-emit.test.ts b/src/frontend/parse-emit.test.ts index 4b07d715..9eeb75b7 100644 --- a/src/frontend/parse-emit.test.ts +++ b/src/frontend/parse-emit.test.ts @@ -4,6 +4,7 @@ describe("Restricted nodes: parse - emit match", () => { for (const t of [ `implicit_conversion "text_to_int" "1";`, `var_declaration $x:Int;`, + `func $x $x;`, `var_declaration_with_assignment ($x:Int <- 0);`, `var_declaration_block (var_declaration $x:Int) (var_declaration $y:Int);`, `many_to_many_assignment {$x; $y} {"x"; "y"};`, @@ -34,6 +35,11 @@ describe("Restricted nodes: parse - emit match", () => { `for_c_like $i $c $a $body;`, `named_arg "name" $x;`, `1:1..1:"int";`, + `def_fn $f $x $y $body;`, + `def_fn_global $f $x $y $body;`, + `def_fn_expanded $f $x;`, + `def_fn_expanded_global $f $x;`, + // `scanning_macro_call $advance $x $y;`, ]) { test(t.split(" ")[0], () => { const normalized = normalize(t); diff --git a/src/frontend/parse.ts b/src/frontend/parse.ts index 83a37c2e..93000aca 100644 --- a/src/frontend/parse.ts +++ b/src/frontend/parse.ts @@ -65,6 +65,8 @@ import { isIdent, postfix, type Text, + functionDefinition, + scanningMacroCall, } from "../IR"; import grammar from "./grammar"; @@ -136,7 +138,7 @@ export function sexpr(callee: Identifier, args: readonly Node[]): Node { return keyValue(args[0], args[1]); case "func": { expectArity(1, Infinity); - const idents = args.slice(0, args.length); + const idents = args.slice(0, -1); const expr = args[args.length - 1]; assertIdentifiers(idents); return func(idents, expr); @@ -346,6 +348,27 @@ export function sexpr(callee: Identifier, args: readonly Node[]): Node { case "named_arg": expectArity(2); return namedArg(asString(args[0]), args[1]); + case "def_fn": + case "def_fn_global": + case "def_fn_expanded": + case "def_fn_expanded_global": { + expectArity(2, Infinity); + const name = args[0]; + assertIdentifier(name); + const idents = args.slice(1, -1); + const body = args[args.length - 1]; + assertIdentifiers(idents); + const opts = { + isGlobal: opCode.includes("global"), + isExpanded: opCode.includes("expanded"), + }; + return functionDefinition(name, idents, body, opts); + } + case "scanning_macro_call": { + expectArity(1, Infinity); + assertIdentifier(args[0]); + return scanningMacroCall(args[0], ...args.slice(1)); + } } if (isOpCode(opCode) && (!restrictedFrontend || isFrontend(opCode))) { if (opCode === "argv_get" && restrictedFrontend) { diff --git a/src/languages/polygolf/emit.ts b/src/languages/polygolf/emit.ts index 0ce9e12e..436cefc7 100644 --- a/src/languages/polygolf/emit.ts +++ b/src/languages/polygolf/emit.ts @@ -313,6 +313,15 @@ function emitNodeWithoutAnnotation( return emitSexpr("@", text(expr.name), expr.value); case "AnyInteger": return emitSexpr("@", expr.low.toString(), expr.high.toString()); + case "FunctionDefinition": { + const def = + "def_fn" + + (expr.isExpanded ? "_expanded" : "") + + (expr.isGlobal ? "_global" : ""); + return emitSexpr(def, expr.name, ...expr.args, expr.body); + } + case "ScanningMacroCall": + return emitSexpr("@", expr.func, ...expr.args); } } diff --git a/src/languages/tex/emit.ts b/src/languages/tex/emit.ts index 90f2f483..605a87f8 100644 --- a/src/languages/tex/emit.ts +++ b/src/languages/tex/emit.ts @@ -82,8 +82,6 @@ class TexEmitter { switch (e.kind) { case "Block": return e.children.map(emit); - case "CapturingBlock": - return this.emitInsideCurlies(e); case "FunctionDefinition": return this.emitDef(e); case "ScanningMacroCall": diff --git a/src/plugins/idents.ts b/src/plugins/idents.ts index 7984ebe6..5edab490 100644 --- a/src/plugins/idents.ts +++ b/src/plugins/idents.ts @@ -1,5 +1,5 @@ import { type Plugin, type IdentifierGenerator } from "common/Language"; -import { getDeclaredIdentifiers, symbolTableRoot } from "../common/symbols"; +import { getDeclaredIdentifiers } from "../common/symbols"; import { type Spine } from "../common/Spine"; import { assignment, From 53da72f8052e129f3b0121161af575ca25461eca Mon Sep 17 00:00:00 2001 From: Jared Hughes Date: Sat, 11 Nov 2023 19:50:40 -0800 Subject: [PATCH 04/10] Support expressions inside prints --- src/common/Spine.ts | 2 +- src/languages/tex/FlatIR.ts | 105 ++++++++++++++++++++++++++++++++++ src/languages/tex/common.ts | 22 +++++++ src/languages/tex/index.ts | 6 +- src/languages/tex/plugins.ts | 71 ++++++----------------- src/languages/tex/tex.test.md | 56 ++++++++++++++++++ 6 files changed, 206 insertions(+), 56 deletions(-) create mode 100644 src/languages/tex/FlatIR.ts create mode 100644 src/languages/tex/common.ts create mode 100644 src/languages/tex/tex.test.md diff --git a/src/common/Spine.ts b/src/common/Spine.ts index 8c49f827..39e3a3d4 100644 --- a/src/common/Spine.ts +++ b/src/common/Spine.ts @@ -178,7 +178,7 @@ export class Spine { } flatMapWithChildrenReplacer( - replacer: Visitor, + replacer: Visitor, ): IR.Node | undefined { if (this.node.kind !== "Block") return; const children = this.node.children; diff --git a/src/languages/tex/FlatIR.ts b/src/languages/tex/FlatIR.ts new file mode 100644 index 00000000..6acca979 --- /dev/null +++ b/src/languages/tex/FlatIR.ts @@ -0,0 +1,105 @@ +import { + assignment, + id, + op, + type IR, + type Node, + mutatingInfix, + type Identifier, +} from "../../IR"; +import { EmitError } from "../../common/emit"; +import { type Immediate, assertIdentifier } from "./common"; + +let globalID = 0; +/** + * This is for converting a tree to a flat list of instructions. + * It's a tree (not a DAG), so the return value of each sub-expr + * is only used in one later expression. + */ +class FlatIRChunk { + private readonly instructions: Node[] = []; + readonly treeID = ++globalID; + + private ipID(ip: number) { + return id(`__tmp_ip_${this.treeID}_${ip}`); + } + + protected pushInstruction(node: Node) { + this.instructions.push(node); + } + + protected addAssignment(rhs: Node): IR.Identifier { + const id = this.ipID(this.instructions.length); + this.instructions.push(assignment(id, rhs)); + return id; + } + + getInstructions(): readonly Node[] { + return this.instructions; + } + + /** + * Return an identifier that can be mutated, or nothing. + * If `right` is false, then assume the returned value has to be freely + * mutateable, i.e. it is a fresh variable. + * Otherwise (`right` is true), the returned value can be an Integer node, + * or a variable that has meaning in outer scope. + */ + addNode(node: Node, right: true): Immediate | null; + addNode(node: Node, right: false): Identifier | null; + addNode(node: Node, right: boolean): Immediate | null { + switch (node.kind) { + case "Assignment": { + assertIdentifier(node.variable, "in LHS of assignment"); + const rhs = this.addNode(node.expr, true); + if (rhs === null) + throw new EmitError(node.expr, "RHS assignment is void"); + this.pushInstruction(assignment(node.variable, rhs)); + return node.variable; + } + case "Integer": + case "Identifier": + // RHS node doesn't need to be a variable reference. + if (right) return node; + // We can't just return `node`, since we expect it to be mutable safely. + return this.addAssignment(node); + case "Infix": { + const { left, right } = this.prepBinary(node.left, node.right); + // Since `left` is only used in this expression, + // and `left` is a variable, we can mutate it. + this.pushInstruction(mutatingInfix(node.name, left, right)); + return left; + } + case "Op": { + if (node.args.length === 1) { + const arg = this.addNode(node.args[0], false); + if (arg === null) + throw new EmitError(node.args[0], "Unary Op arg is void"); + const opres = op(node.op, arg); + // An op like println_int which returns void. + this.pushInstruction(opres); + return null; + } else { + throw new EmitError(node, "flattening op"); + } + } + default: + throw new EmitError(node, "flattening general"); + } + } + + prepBinary(leftNode: Node, rightNode: Node) { + const left = this.addNode(leftNode, false); + const right = this.addNode(rightNode, true); + if (left === null) throw new EmitError(leftNode, "LHS op is void"); + if (right === null) throw new EmitError(rightNode, "RHS op is void"); + return { left, right }; + } +} + +export function convertNodeToListOfStatements(node: Node) { + const chunk = new FlatIRChunk(); + chunk.addNode(node, false); + const insts = chunk.getInstructions(); + return insts; +} diff --git a/src/languages/tex/common.ts b/src/languages/tex/common.ts new file mode 100644 index 00000000..e2a4ec78 --- /dev/null +++ b/src/languages/tex/common.ts @@ -0,0 +1,22 @@ +import { type IR, integerType, isOfKind, textType } from "../../IR"; +import { EmitError } from "../../common/emit"; + +export type Immediate = IR.Identifier | IR.Integer; + +// true = isAscii +export const texStringType = textType(integerType(0, "oo"), true); + +export function assertIdentifier( + n: IR.Node, + detail: string, +): asserts n is IR.Identifier { + if (n.kind !== "Identifier") throw new EmitError(n, detail); +} + +const isImmediate = isOfKind("Identifier", "Integer"); +export function assertImmediate( + n: IR.Node, + detail: string, +): asserts n is Immediate { + if (!isImmediate(n)) throw new EmitError(n, detail); +} diff --git a/src/languages/tex/index.ts b/src/languages/tex/index.ts index a8e39cbb..8e080a50 100644 --- a/src/languages/tex/index.ts +++ b/src/languages/tex/index.ts @@ -7,6 +7,7 @@ import { stuffToMacros, insertAccumulatedCounters, exprTreeToFlat2AC, + bodyToBlock, } from "./plugins"; import { texDetokenizer } from "./detokenizer"; @@ -17,13 +18,11 @@ const texLanguage: Language = { detokenizer: texDetokenizer, phases: [ required( + bodyToBlock, forRangeToWhile, whileToRecursion, - exprTreeToFlat2AC, mapToPrefixAndInfix( { - // TODO: I don't think neg works currently - neg: "-", mul: "\\multiply", // TODO: check if TeX is trunc div or floor div. div: "\\divide", @@ -32,6 +31,7 @@ const texLanguage: Language = { }, true, ), + exprTreeToFlat2AC, stuffToMacros, insertAccumulatedCounters, renameIdents({ diff --git a/src/languages/tex/plugins.ts b/src/languages/tex/plugins.ts index fbc5299e..d1d3115b 100644 --- a/src/languages/tex/plugins.ts +++ b/src/languages/tex/plugins.ts @@ -5,8 +5,6 @@ import { isSubtype, type IR, scanningMacroCall, - textType, - integerType, text, id, varDeclaration, @@ -15,58 +13,39 @@ import { type Node, voidType, type MutatingInfix, - assignment, - op, isOfKind, } from "../../IR"; import { getType } from "../../common/getType"; import { EmitError } from "../../common/emit"; import { type Spine } from "../../common/Spine"; +import { convertNodeToListOfStatements } from "./FlatIR"; +import { assertIdentifier, assertImmediate, texStringType } from "./common"; -type Immediate = IR.Identifier | IR.Integer; - -// true = isAscii -const texStringType = textType(integerType(0, "oo"), true); +/** Convert all (while, for, etc.) bodies to be blocks. + * Block behaves as a "sequence"/"group" node that's just for allowing + * multiple statements to go into one expression. */ +export const bodyToBlock: Plugin = { + name: "bodyToBlock", + visit(node, spine) { + if (spine.isRoot || spine.pathFragment === "body") { + return block([node]); + // return node; + } + }, +}; // TODO: I don't know what's the actual term. 3 argument code? export const exprTreeToFlat2AC: Plugin = { name: "exprTreeToFlat2AC", visit(_node, spine) { - return spine.flatMapWithChildrenReplacer(exprTreeToFlat2ACVisitor); + return spine.flatMapWithChildrenReplacer((node, spine) => { + if (spine.parent?.node.kind !== "Block") return; + if (isOfKind("Assignment", "Op")(node)) + return convertNodeToListOfStatements(node); + }); }, }; -let globalID = 0; -function exprTreeToFlat2ACVisitor(node: IR.Node, spine: Spine) { - if (node.kind !== "Assignment") return; - if (node.variable.kind !== "Identifier") return; - // if (!isSubtype(getType(node, spine), int32Type)) return; - const treeID = ++globalID; - function ipID(ip: number) { - return id(`__tmp_ip_${treeID}_${ip}`); - } - const flat: IR.Assignment[] = []; - function rec(n: IR.Node): Immediate { - if (n.kind === "Integer") return n; - if (n.kind === "Identifier") return n; - if (n.kind !== "Op") throw new EmitError(n, "tree has a not-Op"); - if (n.args.length !== 2) throw new EmitError(n, "not two args"); - // left - const left = rec(n.args[0]); - const newVar = ipID(flat.length); - const node = assignment(newVar, left); - flat.push(node); - // right, and compute. - const right = rec(n.args[1]); - const opres = op(n.op, newVar, right); - flat.push(assignment(newVar, opres)); - return newVar; - } - const res = rec(node.expr); - flat.push(assignment(node.variable, res)); - return flat; -} - /** Bad global state. Insert strings that will need to be counter names. */ const accumulatedCounters = new Set(); @@ -209,15 +188,3 @@ export const insertAccumulatedCounters: Plugin = { function voidIt(n: Node) { return { ...n, type: voidType }; } - -function assertIdentifier( - n: IR.Node, - detail: string, -): asserts n is IR.Identifier { - if (n.kind !== "Identifier") throw new EmitError(n, detail); -} - -const isImmediate = isOfKind("Identifier", "Integer"); -function assertImmediate(n: IR.Node, detail: string): asserts n is Immediate { - if (!isImmediate(n)) throw new EmitError(n, detail); -} diff --git a/src/languages/tex/tex.test.md b/src/languages/tex/tex.test.md new file mode 100644 index 00000000..5bab65f7 --- /dev/null +++ b/src/languages/tex/tex.test.md @@ -0,0 +1,56 @@ +# TeX + +## Ops emit + +```polygolf +$n:0..1 <- 1; +$m:-50..50 <- $n; + +println_int $m; + +$m <- $n; +$m <- (- $n); + +$m <- ($n * 3); +$m <- ($n div 4); +$m <- ($n + 5); +% $m <- ($n - 6); % TODO-tex +``` + +```tex nogolf +\newcount\n\newcount\m\newcount\t\newcount\T\newcount~\newcount\a\newcount\b\n1 \m\n\t\m\the\t\endgraf\m\n\T-1 \multiply\T\n\m\T~3 \multiply~\n\m~\a\n\divide\a4 \m\a\b5 \advance\b\n\m\b +``` + +## If statement emit + +```polygolf +$n:0..1 <- 1; +$m:-50..50 <- $n; +if ($n < $m) { $m <- 12; }; +if ($n < $m) { $m <- 13; } { $m <- 14; }; +if ($n > $m) { $m <- 15; }; +if ($n > $m) { $m <- 16; } { $m <- 17; }; +if ($n == $m) { $m <- 18; }; +if ($n == $m) { $m <- 19; } { $m <- 20; }; +% if ($n <= $m) { $m <- 21; }; % TODO-tex +% if ($n <= $m) { $m <- 22; } { $m <- 23; }; % TODO-tex +% if ($n >= $m) { $m <- 24; }; % TODO-tex +% if ($n >= $m) { $m <- 25; } { $m <- 26; }; % TODO-tex +% TODO-tex: ANDing conditions should be nested loops. +``` + +```tex nogolf +\newcount\n\newcount\m\n1 \m\n\ifnum\n<\m\m12 \fi\ifnum\n<\m\m13 \else\m14 \fi\ifnum\n>\m\m15 \fi\ifnum\n>\m\m16 \else\m17 \fi\ifnum\n=\m\m18 \fi\ifnum\n=\m\m19 \else\m20 \fi +``` + +## Looping + +```polygolf +for $i 0 31 { + println_int ((1 + $i) + ($i * $i)); +}; +``` + +```tex nogolf +\newcount\i\newcount\t\newcount\T\i0 \def~{\ifnum\i<31 \t1 \advance\t\i\T\i\multiply\T\i\advance\t\T\the\t\endgraf\advance\i1 ~\fi}~ +``` From 958ad0d9e2ac01ff619b7a8c2042a5e993a67193 Mon Sep 17 00:00:00 2001 From: Jared Hughes Date: Sat, 11 Nov 2023 20:03:58 -0800 Subject: [PATCH 05/10] Revert forRangeToWhile block requirement Had accidentally changed it so that would only work when the node was a child of a block, which turned out to be not always. --- src/languages/tex/index.ts | 2 -- src/languages/tex/plugins.ts | 13 ----------- src/plugins/loops.ts | 42 ++++++++++++++++-------------------- 3 files changed, 19 insertions(+), 38 deletions(-) diff --git a/src/languages/tex/index.ts b/src/languages/tex/index.ts index 8e080a50..dfd24a41 100644 --- a/src/languages/tex/index.ts +++ b/src/languages/tex/index.ts @@ -7,7 +7,6 @@ import { stuffToMacros, insertAccumulatedCounters, exprTreeToFlat2AC, - bodyToBlock, } from "./plugins"; import { texDetokenizer } from "./detokenizer"; @@ -18,7 +17,6 @@ const texLanguage: Language = { detokenizer: texDetokenizer, phases: [ required( - bodyToBlock, forRangeToWhile, whileToRecursion, mapToPrefixAndInfix( diff --git a/src/languages/tex/plugins.ts b/src/languages/tex/plugins.ts index d1d3115b..e47bfdc6 100644 --- a/src/languages/tex/plugins.ts +++ b/src/languages/tex/plugins.ts @@ -21,19 +21,6 @@ import { type Spine } from "../../common/Spine"; import { convertNodeToListOfStatements } from "./FlatIR"; import { assertIdentifier, assertImmediate, texStringType } from "./common"; -/** Convert all (while, for, etc.) bodies to be blocks. - * Block behaves as a "sequence"/"group" node that's just for allowing - * multiple statements to go into one expression. */ -export const bodyToBlock: Plugin = { - name: "bodyToBlock", - visit(node, spine) { - if (spine.isRoot || spine.pathFragment === "body") { - return block([node]); - // return node; - } - }, -}; - // TODO: I don't know what's the actual term. 3 argument code? export const exprTreeToFlat2AC: Plugin = { name: "exprTreeToFlat2AC", diff --git a/src/plugins/loops.ts b/src/plugins/loops.ts index 8afef3f4..9ed36c70 100644 --- a/src/plugins/loops.ts +++ b/src/plugins/loops.ts @@ -59,32 +59,28 @@ export function forRangeToForRangeInclusive(skip1Step = false): Plugin { export const forRangeToWhile: Plugin = { name: "forRangeToWhile", - visit(_node, spine) { - return spine.flatMapWithChildrenReplacer(forRangeToWhileVisitor); + visit(node, spine) { + if (node.kind === "ForRange" && node.variable !== undefined) { + const low = getType(node.start, spine); + const high = getType(node.end, spine); + if (low.kind !== "integer" || high.kind !== "integer") { + throw new Error(`Unexpected type (${low.kind},${high.kind})`); + } + const increment = assignment( + node.variable, + op("add", node.variable, node.increment), + ); + return block([ + assignment(node.variable, node.start), + whileLoop( + op(node.inclusive ? "leq" : "lt", node.variable, node.end), + block([node.body, increment]), + ), + ]); + } }, }; -function forRangeToWhileVisitor(node: IR.Node, spine: Spine) { - if (node.kind === "ForRange" && node.variable !== undefined) { - const low = getType(node.start, spine); - const high = getType(node.end, spine); - if (low.kind !== "integer" || high.kind !== "integer") { - throw new Error(`Unexpected type (${low.kind},${high.kind})`); - } - const increment = assignment( - node.variable, - op("add", node.variable, node.increment), - ); - return [ - assignment(node.variable, node.start), - whileLoop( - op(node.inclusive ? "leq" : "lt", node.variable, node.end), - block([node.body, increment]), - ), - ]; - } -} - export const forRangeToForCLike: Plugin = { name: "forRangeToForCLike", visit(node, spine) { From 1fdf278c8fe3240470fb0ce8856741c3d7054b5c Mon Sep 17 00:00:00 2001 From: Jared Hughes Date: Sun, 12 Nov 2023 02:54:27 -0800 Subject: [PATCH 06/10] Handle for-range either in block or at root. Reverts "revert forRangeToWhile block requirement" Now, `forRangeToWhile` will only apply to a ForRange that is either the direct child of a block, or is the root node. The test was failing because the plugin didn't apply to root node. But the old implementation using visit() directly didn't work because error "attempt to insert a block into a block". --- src/common/Spine.ts | 4 ++++ src/plugins/loops.test.md | 18 +++++++++++++++++ src/plugins/loops.ts | 42 +++++++++++++++++++++------------------ 3 files changed, 45 insertions(+), 19 deletions(-) diff --git a/src/common/Spine.ts b/src/common/Spine.ts index 39e3a3d4..2a33140f 100644 --- a/src/common/Spine.ts +++ b/src/common/Spine.ts @@ -180,6 +180,10 @@ export class Spine { flatMapWithChildrenReplacer( replacer: Visitor, ): IR.Node | undefined { + if (this.isRoot) { + const repl = replacer(this.node, this); + if (repl !== undefined) return block(repl); + } if (this.node.kind !== "Block") return; const children = this.node.children; let newChildren: IR.Node[] | undefined; diff --git a/src/plugins/loops.test.md b/src/plugins/loops.test.md index 219349e1..26d1a399 100644 --- a/src/plugins/loops.test.md +++ b/src/plugins/loops.test.md @@ -28,6 +28,24 @@ for_c_like ($i <- 0) ($i < 10) ($i <- (1 + $i)) ( ); ``` +## For range to while, not at root + +```polygolf +$a <- 0; +for $i 0 10 { + print_int $x; +}; +``` + +```polygolf loops.forRangeToWhile +$a <- 0; +$i <- 0; +while ($i < 10) { + print_int $x; + $i <- (1 + $i); +}; +``` + ## For each ```polygolf diff --git a/src/plugins/loops.ts b/src/plugins/loops.ts index 9ed36c70..8afef3f4 100644 --- a/src/plugins/loops.ts +++ b/src/plugins/loops.ts @@ -59,28 +59,32 @@ export function forRangeToForRangeInclusive(skip1Step = false): Plugin { export const forRangeToWhile: Plugin = { name: "forRangeToWhile", - visit(node, spine) { - if (node.kind === "ForRange" && node.variable !== undefined) { - const low = getType(node.start, spine); - const high = getType(node.end, spine); - if (low.kind !== "integer" || high.kind !== "integer") { - throw new Error(`Unexpected type (${low.kind},${high.kind})`); - } - const increment = assignment( - node.variable, - op("add", node.variable, node.increment), - ); - return block([ - assignment(node.variable, node.start), - whileLoop( - op(node.inclusive ? "leq" : "lt", node.variable, node.end), - block([node.body, increment]), - ), - ]); - } + visit(_node, spine) { + return spine.flatMapWithChildrenReplacer(forRangeToWhileVisitor); }, }; +function forRangeToWhileVisitor(node: IR.Node, spine: Spine) { + if (node.kind === "ForRange" && node.variable !== undefined) { + const low = getType(node.start, spine); + const high = getType(node.end, spine); + if (low.kind !== "integer" || high.kind !== "integer") { + throw new Error(`Unexpected type (${low.kind},${high.kind})`); + } + const increment = assignment( + node.variable, + op("add", node.variable, node.increment), + ); + return [ + assignment(node.variable, node.start), + whileLoop( + op(node.inclusive ? "leq" : "lt", node.variable, node.end), + block([node.body, increment]), + ), + ]; + } +} + export const forRangeToForCLike: Plugin = { name: "forRangeToForCLike", visit(node, spine) { From a0479dfbfd0701bb1d4d3d753c7af4dca4c86503 Mon Sep 17 00:00:00 2001 From: Jared Hughes Date: Sun, 12 Nov 2023 05:06:22 -0800 Subject: [PATCH 07/10] Support mod, sub, calculation inside comparison --- src/IR/functions.ts | 7 ++++ src/common/Spine.ts | 11 ++++- src/common/fragments.ts | 5 +++ src/common/symbols.ts | 31 ++++++++++++-- src/languages/tex/FlatIR.ts | 32 +++++++++----- src/languages/tex/emit.ts | 17 +------- src/languages/tex/index.ts | 8 +++- src/languages/tex/plugins.ts | 79 +++++++++++++++++++++++++++++++++-- src/languages/tex/tex.test.md | 34 ++++++++++++++- src/plugins/imports.ts | 10 +++++ src/plugins/loops.ts | 7 +--- 11 files changed, 196 insertions(+), 45 deletions(-) diff --git a/src/IR/functions.ts b/src/IR/functions.ts index f5cf6a18..9ab7f1ed 100644 --- a/src/IR/functions.ts +++ b/src/IR/functions.ts @@ -1,3 +1,4 @@ +import type { Spine } from "../common/Spine"; import { type Identifier, type BaseNode, @@ -56,3 +57,9 @@ export function functionDefinition( isExpanded, }; } + +export function functionDefinitionNestingDepth(s: Spine, d: number = 0) { + if (s.node.kind === "FunctionDefinition") ++d; + if (s.parent === null) return d; + return functionDefinitionNestingDepth(s.parent, d); +} diff --git a/src/common/Spine.ts b/src/common/Spine.ts index 2a33140f..3629b5c8 100644 --- a/src/common/Spine.ts +++ b/src/common/Spine.ts @@ -1,6 +1,11 @@ import { type IR, isOp, op, block } from "../IR"; import { type CompilationContext } from "./compile"; -import { getChild, getChildFragments, type PathFragment } from "./fragments"; +import { + getChild, + getChildFragments, + getPathProp, + type PathFragment, +} from "./fragments"; import { replaceAtIndex } from "./arrays"; /** A Spine points to one node and keeps track of all of its ancestors up to @@ -24,6 +29,10 @@ export class Spine { return this.parent === null; } + getPathProp() { + return this.pathFragment !== null ? getPathProp(this.pathFragment) : null; + } + /** Get a list of all child spines. */ getChildSpines(): Spine[] { return Array.from(getChildFragments(this.node)).map((n) => diff --git a/src/common/fragments.ts b/src/common/fragments.ts index a1d14d46..a94089e8 100644 --- a/src/common/fragments.ts +++ b/src/common/fragments.ts @@ -17,6 +17,11 @@ export type PathFragment = readonly index: number; }; +export function getPathProp(frag: PathFragment) { + if (typeof frag === "string") return frag; + else return frag.prop; +} + export function getChild(node: IR.Node, pathFragment: PathFragment): IR.Node { if (typeof pathFragment === "string") { return (node as any)[pathFragment]; diff --git a/src/common/symbols.ts b/src/common/symbols.ts index 328284ac..fc6bdab9 100644 --- a/src/common/symbols.ts +++ b/src/common/symbols.ts @@ -121,6 +121,13 @@ function introducedSymbols( return [node.variable.name]; case "FunctionDefinition": return [node.name.name]; + case "Identifier": + if ( + spine.parent?.node.kind === "FunctionDefinition" && + spine.getPathProp() === "args" + ) + return [node.name]; + return undefined; } } @@ -185,11 +192,21 @@ function getTypeFromBinding(name: string, spine: Spine): Type { ); return node.variable.type ?? assignedType; } - default: - throw new Error( - `Programming error: node of type ${node.kind} does not bind any symbol`, - ); + case "Identifier": + if ( + spine.parent?.node.kind === "FunctionDefinition" && + spine.getPathProp() === "args" + ) { + if (node.type !== undefined) return node.type; + throw new PolygolfError( + `Programming error: function parameter '${node.name}' missing annotated type.`, + ); + } + break; } + throw new PolygolfError( + `Programming error: node of type ${node.kind} does not bind any symbol`, + ); } export interface VarAccess { @@ -335,3 +352,9 @@ export function readsFromArgv(node: Node): boolean { export function readsFromInput(node: Node): boolean { return readsFromArgv(node) || readsFromStdin(node); } + +// TODO: global counter here silly; +let globalID = 0; +export function tempId() { + return `__tmp_id_${globalID++}`; +} diff --git a/src/languages/tex/FlatIR.ts b/src/languages/tex/FlatIR.ts index 6acca979..29ebf070 100644 --- a/src/languages/tex/FlatIR.ts +++ b/src/languages/tex/FlatIR.ts @@ -5,7 +5,7 @@ import { type IR, type Node, mutatingInfix, - type Identifier, + ifStatement, } from "../../IR"; import { EmitError } from "../../common/emit"; import { type Immediate, assertIdentifier } from "./common"; @@ -45,8 +45,8 @@ class FlatIRChunk { * Otherwise (`right` is true), the returned value can be an Integer node, * or a variable that has meaning in outer scope. */ - addNode(node: Node, right: true): Immediate | null; - addNode(node: Node, right: false): Identifier | null; + // TODO: rename `!right` to `mustReturnScratchCounter`. + // TODO: Instead of that, handling inside the one case of mustReturnScratchCounter=true? addNode(node: Node, right: boolean): Immediate | null { switch (node.kind) { case "Assignment": { @@ -64,7 +64,8 @@ class FlatIRChunk { // We can't just return `node`, since we expect it to be mutable safely. return this.addAssignment(node); case "Infix": { - const { left, right } = this.prepBinary(node.left, node.right); + const left = this.addNodeRequired(node.left, false) as any; + const right = this.addNodeRequired(node.right, true); // Since `left` is only used in this expression, // and `left` is a variable, we can mutate it. this.pushInstruction(mutatingInfix(node.name, left, right)); @@ -72,7 +73,7 @@ class FlatIRChunk { } case "Op": { if (node.args.length === 1) { - const arg = this.addNode(node.args[0], false); + const arg = this.addNode(node.args[0], true); if (arg === null) throw new EmitError(node.args[0], "Unary Op arg is void"); const opres = op(node.op, arg); @@ -83,17 +84,26 @@ class FlatIRChunk { throw new EmitError(node, "flattening op"); } } + case "If": { + const cond = node.condition; + if (cond.kind !== "Op" || cond.args.length !== 2) + throw new EmitError(cond, "flattening if condition"); + const left = this.addNodeRequired(cond.args[0], true); + const right = this.addNodeRequired(cond.args[1], true); + const newCond = op(cond.op, left, right); + const stmt = ifStatement(newCond, node.consequent, node.alternate); + this.pushInstruction(stmt); + return null; + } default: throw new EmitError(node, "flattening general"); } } - prepBinary(leftNode: Node, rightNode: Node) { - const left = this.addNode(leftNode, false); - const right = this.addNode(rightNode, true); - if (left === null) throw new EmitError(leftNode, "LHS op is void"); - if (right === null) throw new EmitError(rightNode, "RHS op is void"); - return { left, right }; + addNodeRequired(node: Node, right: boolean) { + const added = this.addNode(node, right); + if (added === null) throw new EmitError(node, "operand is void"); + return added; } } diff --git a/src/languages/tex/emit.ts b/src/languages/tex/emit.ts index 605a87f8..8827bc6d 100644 --- a/src/languages/tex/emit.ts +++ b/src/languages/tex/emit.ts @@ -15,8 +15,6 @@ export default function emitProgram( return new TexEmitter(program, context).emitProgram(); } -const macroParamRegex = /^#[1-9]$/; - interface EmitContext { /** * `scanningFor` is currently not used. I introduced it because I forgot you're @@ -44,12 +42,6 @@ class TexEmitter { private readonly emitContextStack: EmitContext[] = []; - private pushScanningFor(s: string) { - this.pushContext({ - scanningFor: [...this.emitContext.scanningFor, s], - }); - } - private pushContext(c: Partial) { this.emitContextStack.push(this.emitContext); this.emitContext = { ...this.emitContext, ...c }; @@ -103,13 +95,6 @@ class TexEmitter { } private emitDef(e: IR.FunctionDefinition): TokenTree { - const ids = e.args.map((id) => { - if (!macroParamRegex.test(id.name)) - throw new EmitError(id, "Invalid macro parameter."); - const depth = this.emitContext.macroDepth; - if (depth >= 3) throw new Error("Macro definitions nested too far"); - return "#".repeat(2 ** depth) + id.name[1]; - }); const slashDef = e.isGlobal ? e.isExpanded ? "\\xdef" @@ -118,6 +103,6 @@ class TexEmitter { ? "\\edef" : "\\def"; const body = this.emitInsideCurlies(e.body); - return [slashDef, e.name.name, ids, body]; + return [slashDef, e.name.name, e.args.map((id) => id.name), body]; } } diff --git a/src/languages/tex/index.ts b/src/languages/tex/index.ts index dfd24a41..70215a89 100644 --- a/src/languages/tex/index.ts +++ b/src/languages/tex/index.ts @@ -7,6 +7,8 @@ import { stuffToMacros, insertAccumulatedCounters, exprTreeToFlat2AC, + addTeXHelpers, + macroParamsToHash, } from "./plugins"; import { texDetokenizer } from "./detokenizer"; @@ -22,14 +24,17 @@ const texLanguage: Language = { mapToPrefixAndInfix( { mul: "\\multiply", + /** helper_mod is defined in addTeXImports */ + mod: "helper_mod", // TODO: check if TeX is trunc div or floor div. div: "\\divide", add: "\\advance", - // TODO: sub works with \\advance- + sub: "helper_sub", }, true, ), exprTreeToFlat2AC, + addTeXHelpers, stuffToMacros, insertAccumulatedCounters, renameIdents({ @@ -37,6 +42,7 @@ const texLanguage: Language = { short: ["~"].concat(lettersOnlyIdentGen.short.map((c) => "\\" + c)), general: (i) => "\\" + lettersOnlyIdentGen.general(i), }), + macroParamsToHash, ), ], }; diff --git a/src/languages/tex/plugins.ts b/src/languages/tex/plugins.ts index e47bfdc6..96c1adff 100644 --- a/src/languages/tex/plugins.ts +++ b/src/languages/tex/plugins.ts @@ -14,12 +14,18 @@ import { voidType, type MutatingInfix, isOfKind, + functionCall, + functionDefinitionNestingDepth, + type Type, + type Identifier, + builtin, } from "../../IR"; import { getType } from "../../common/getType"; import { EmitError } from "../../common/emit"; import { type Spine } from "../../common/Spine"; import { convertNodeToListOfStatements } from "./FlatIR"; import { assertIdentifier, assertImmediate, texStringType } from "./common"; +import { addDefinitions } from "../../plugins/imports"; // TODO: I don't know what's the actual term. 3 argument code? export const exprTreeToFlat2AC: Plugin = { @@ -27,13 +33,17 @@ export const exprTreeToFlat2AC: Plugin = { visit(_node, spine) { return spine.flatMapWithChildrenReplacer((node, spine) => { if (spine.parent?.node.kind !== "Block") return; - if (isOfKind("Assignment", "Op")(node)) + if (isOfKind("Assignment", "Op", "If")(node)) return convertNodeToListOfStatements(node); }); }, }; -/** Bad global state. Insert strings that will need to be counter names. */ +/** + * Bad global state. Insert strings that will need to be counter names. + * This should really be replaced with a `compactMap`, but I didn't have an + * easy way to detect what variables are counters. Seems should be easy though. + */ const accumulatedCounters = new Set(); export const stuffToMacros: Plugin = { @@ -116,8 +126,23 @@ function mutatingInfixToMacros(node: MutatingInfix, spine: Spine) { // } // return ["\\advance", variable.name, "-", this.emit(right)]; accumulatedCounters.add(variable.name); - const macroName = id(node.name, true); - return voidIt(scanningMacroCall(macroName, variable, right)); + const { name } = node; + if (name === "helper_sub") { + // TODO: handle negative `right`. + return voidIt( + scanningMacroCall(builtin("\\advance"), variable, text("-"), right), + ); + } + // \advance, \multiply, \divide gobble up numbers on second argument, + // so they don't need curly braces. + const isScan = + name === "\\advance" || name === "\\multiply" || name === "\\divide"; + const macroName = id(name, isScan); + if (isScan) { + return voidIt(scanningMacroCall(macroName, variable, right)); + } else { + return voidIt(functionCall(macroName, variable, right)); + } } else { throw new EmitError(node, "not integer"); } @@ -172,6 +197,52 @@ export const insertAccumulatedCounters: Plugin = { }, }; +function idWithType(name: string, type: Type): Identifier { + return { ...id(name), type }; +} + +const modX = idWithType("helper_mod_x", int32Type); +const modY = idWithType("helper_mod_y", int32Type); +const modTmp = idWithType("helper_mod_tmp", int32Type); +const modImpl = functionDefinition( + "helper_mod", + [modX, modY], + block([ + scanningMacroCall(modTmp, modX), + scanningMacroCall(builtin("\\divide"), modTmp, modY), + scanningMacroCall(builtin("\\multiply"), modTmp, modY), + scanningMacroCall(builtin("\\advance"), modX, text("-"), modTmp), + ]), +); +export const addTeXHelpers: Plugin = addDefinitions({ + /** + * helper_mod is a macro that behaves like \divide except returning the modulo, + * and the second argument must be a proper argument (can't scan for number). + */ + helper_mod: () => { + accumulatedCounters.add(modTmp.name); + return modImpl; + }, +}); + +export const macroParamsToHash: Plugin = { + name: "macroParamsToHash", + visit(node, spine) { + if (node.kind !== "FunctionDefinition" || node.args.length === 0) return; + const depth = functionDefinitionNestingDepth(spine) - 1; + const hash = "#".repeat(2 ** depth); + const identMap = new Map( + node.args.map((ident, i) => [ident.name, id(`${hash}${i + 1}`)]), + ); + return spine.withReplacer((n) => { + if (n.kind !== "Identifier") return; + const g = identMap.get(n.name); + if (g === undefined) return; + return g; + }).node; + }, +}; + function voidIt(n: Node) { return { ...n, type: voidType }; } diff --git a/src/languages/tex/tex.test.md b/src/languages/tex/tex.test.md index 5bab65f7..046784dd 100644 --- a/src/languages/tex/tex.test.md +++ b/src/languages/tex/tex.test.md @@ -14,11 +14,24 @@ $m <- (- $n); $m <- ($n * 3); $m <- ($n div 4); $m <- ($n + 5); -% $m <- ($n - 6); % TODO-tex +$m <- ($n - 6); +$m <- ($m - $n):-50..50; ``` ```tex nogolf -\newcount\n\newcount\m\newcount\t\newcount\T\newcount~\newcount\a\newcount\b\n1 \m\n\t\m\the\t\endgraf\m\n\T-1 \multiply\T\n\m\T~3 \multiply~\n\m~\a\n\divide\a4 \m\a\b5 \advance\b\n\m\b +\newcount\n\newcount\m\newcount\t\newcount\T\newcount~\newcount\a\newcount\b\n1 \m\n\the\m\endgraf\m\n\t-1 \multiply\t\n\m\t\T3 \multiply\T\n\m\T~\n\divide~4 \m~\a5 \advance\a\n\m\a\b\n\advance\b-6 \m\b\advance\m-\n +``` + +## Op mod + +```polygolf +$n <- 0; +$m <- 1; +$n <- ($n mod $m); +``` + +```tex nogolf +\newcount\h\newcount\n\newcount\m\def\H#1#2{\h#1\divide\h#2\multiply\h#2\advance#1-\h}\n0 \m1 \H{\n}{\m} ``` ## If statement emit @@ -54,3 +67,20 @@ for $i 0 31 { ```tex nogolf \newcount\i\newcount\t\newcount\T\i0 \def~{\ifnum\i<31 \t1 \advance\t\i\T\i\multiply\T\i\advance\t\T\the\t\endgraf\advance\i1 ~\fi}~ ``` + +## Mod inside if statement + +```polygolf +$i <- 10; +$sum:0..9999 <- 0; +for $d 1 $i { + if (($i mod $d) < 1) { + $sum <- ($sum + $d):0..9999; + }; +}; +println_int $sum; +``` + +```tex nogolf +\newcount\h\newcount\i\newcount\s\newcount\d\newcount\t\def\H#1#2{\h#1\divide\h#2\multiply\h#2\advance#1-\h}\i10 \s0 \d1 \def\T{\ifnum\d<\i\t\i\H{\t}{\d}\ifnum\t<1 \advance\s\d\fi\advance\d1 \T\fi}\T\the\s\endgraf +``` diff --git a/src/plugins/imports.ts b/src/plugins/imports.ts index 7cc4cbd1..8b6e07c8 100644 --- a/src/plugins/imports.ts +++ b/src/plugins/imports.ts @@ -34,3 +34,13 @@ export function addImports( // TODO caching }, }; } + +export function addDefinitions(rules: Record Node>): Plugin { + return addImports( + Object.fromEntries([...Object.keys(rules)].map((k) => [k, k])), + (ks: string[]) => { + if (ks.length === 0) return undefined; + return block(ks.map((k) => rules[k]())); + }, + ); +} diff --git a/src/plugins/loops.ts b/src/plugins/loops.ts index 8afef3f4..e7478140 100644 --- a/src/plugins/loops.ts +++ b/src/plugins/loops.ts @@ -35,6 +35,7 @@ import { } from "../IR"; import { byteLength, charLength } from "../common/objective"; import { PolygolfError } from "../common/errors"; +import { tempId } from "../common/symbols"; export function forRangeToForRangeInclusive(skip1Step = false): Plugin { return { @@ -445,12 +446,6 @@ export const removeUnusedForVar: Plugin = { }, }; -// TODO: global counter here silly; -let globalID = 0; -function tempId() { - return `__tmp_id_${globalID++}`; -} - export const whileToRecursion: Plugin = { name: "whileToRecursion", visit(_node, spine) { From 7aa5dee3ba9c5dabb40199f9ad2fe1dbf76a261c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20Mar=C5=A1=C3=A1lek?= Date: Thu, 28 Dec 2023 00:43:28 +0100 Subject: [PATCH 08/10] merge main --- .prettierignore | 1 + .prettierrc | 2 +- README.md | 47 +- docs/architecture.md | 2 + docs/opcodes.generated.md | 86 ++ package-lock.json | 14 + package.json | 16 +- src/IR/assignments.ts | 6 +- src/IR/exprs.ts | 171 ++-- src/IR/opcodes.ts | 841 +++++++++++++------- src/IR/terminals.ts | 6 +- src/IR/types.ts | 158 +++- src/cli.ts | 20 +- src/common/Language.ts | 39 +- src/common/Spine.ts | 118 +-- src/common/arrays.ts | 10 +- src/common/compile.test.ts | 2 +- src/common/compile.ts | 275 +++++-- src/common/emit.ts | 4 +- src/common/fragments.ts | 5 +- src/common/getType.test.ts | 150 ++-- src/common/getType.ts | 760 +++++++++--------- src/common/objective.ts | 65 +- src/common/strings.ts | 92 +++ src/common/symbols.ts | 12 +- src/cover/index.ts | 221 +++++ src/docs-gen/index.ts | 46 ++ src/frontend/grammar.ne | 2 +- src/frontend/lexer.ts | 28 +- src/frontend/parse-emit.test.ts | 4 +- src/frontend/parse.test.ts | 58 +- src/frontend/parse.ts | 231 ++++-- src/interpreter/index.ts | 86 ++ src/languages/golfscript/emit.ts | 56 +- src/languages/golfscript/golfscript.test.md | 89 ++- src/languages/golfscript/index.ts | 163 ++-- src/languages/janet/emit.ts | 148 ++++ src/languages/janet/index.ts | 206 +++++ src/languages/janet/janet.test.md | 196 +++++ src/languages/janet/plugins.ts | 12 + src/languages/javascript/emit.ts | 17 +- src/languages/javascript/index.ts | 151 +++- src/languages/javascript/javascript.test.md | 44 +- src/languages/javascript/plugins.ts | 70 +- src/languages/languages.ts | 6 +- src/languages/lua/emit.ts | 9 +- src/languages/lua/index.ts | 103 ++- src/languages/lua/lua.test.md | 42 +- src/languages/lua/plugins.ts | 36 +- src/languages/nim/emit.ts | 97 +-- src/languages/nim/index.ts | 148 +++- src/languages/nim/nim.test.md | 150 ++-- src/languages/nim/plugins.ts | 109 ++- src/languages/polygolf/emit.ts | 117 ++- src/languages/python/emit.ts | 32 +- src/languages/python/index.ts | 192 ++++- src/languages/python/plugins.ts | 55 ++ src/languages/python/python.test.md | 85 +- src/languages/swift/emit.ts | 27 +- src/languages/swift/index.ts | 188 ++++- src/languages/swift/swift.test.md | 119 ++- src/languages/tex/FlatIR.ts | 4 +- src/languages/tex/index.ts | 5 +- src/languages/tex/plugins.ts | 16 +- src/languages/text/index.ts | 11 + src/languages/text/text.test.md | 37 + src/markdown-tests/index.ts | 33 +- src/plugins/arithmetic.ts | 374 +++++---- src/plugins/block.test.md | 8 +- src/plugins/block.ts | 196 +++-- src/plugins/conditions.test.md | 17 + src/plugins/conditions.ts | 107 +++ src/plugins/idents.test.md | 4 + src/plugins/idents.ts | 50 +- src/plugins/imports.ts | 4 +- src/plugins/loops.test.md | 60 +- src/plugins/loops.ts | 357 ++++----- src/plugins/ops.test.md | 2 +- src/plugins/ops.ts | 261 ++++-- src/plugins/packing.ts | 94 +-- src/plugins/print.test.md | 56 ++ src/plugins/print.ts | 181 ++++- src/plugins/static.test.md | 6 +- src/plugins/static.ts | 54 +- src/plugins/tables.test.md | 12 +- src/plugins/tables.ts | 37 +- src/plugins/textOps.test.md | 60 +- src/plugins/textOps.ts | 139 ++-- src/plugins/types.ts | 100 ++- src/programs/code.golf-default.test.md | 14 +- 90 files changed, 5739 insertions(+), 2805 deletions(-) create mode 100644 docs/opcodes.generated.md create mode 100644 src/common/strings.ts create mode 100644 src/cover/index.ts create mode 100644 src/docs-gen/index.ts create mode 100644 src/interpreter/index.ts create mode 100644 src/languages/janet/emit.ts create mode 100644 src/languages/janet/index.ts create mode 100644 src/languages/janet/janet.test.md create mode 100644 src/languages/janet/plugins.ts create mode 100644 src/languages/python/plugins.ts create mode 100644 src/languages/text/index.ts create mode 100644 src/languages/text/text.test.md create mode 100644 src/plugins/conditions.test.md create mode 100644 src/plugins/conditions.ts diff --git a/.prettierignore b/.prettierignore index 2998432a..dc3fa425 100644 --- a/.prettierignore +++ b/.prettierignore @@ -2,3 +2,4 @@ node_modules dist src/frontend/grammar.ts *.test.md.ts +*.generated.md \ No newline at end of file diff --git a/.prettierrc b/.prettierrc index 0967ef42..5c2e4283 100644 --- a/.prettierrc +++ b/.prettierrc @@ -1 +1 @@ -{} +{ "embeddedLanguageFormatting": "off" } diff --git a/README.md b/README.md index fb7fd358..0889af0f 100644 --- a/README.md +++ b/README.md @@ -49,13 +49,14 @@ PolyGolf is designed to be decent at golfing, so there's concern about making it ## Syntax -Program is a sequence of expressions. -Expression is either +Program is a tree of nodes. +Node is either - integer literal `58`, - text literal `"text literal\n another line"`, - variable `$very_important_var`, -- a block, potentially with multiple variants `{ variant1 / variant2 / variant3 }` or +- root block `op1; op2; op3;` +- a block `{op1; op2; op3}`, potentially with multiple variants `{ variant1 / variant2 / variant3 }` or - s-expression S-expression takes one of the following forms: @@ -140,36 +141,16 @@ Each variable must be first used in an assignment. Variable type is determined b ### Polygolf operators -All other expressions are Polygolf operators. Most of them return values, but some are used for I/O and some are used for setting values in collections. -[Complete list of builtins](https://github.com/polygolf-lang/polygolf/blob/main/src/IR/opcodes.ts). -All of the Polygolf operators can be called using their name. In addition, several common ops are given symbolic aliases: - -| Op name | alias | -| --------------- | ----- | -| add | + | -| sub/neg | - | -| mul | \* | -| pow | ^ | -| bit_and | & | -| bit_or | \| | -| bit_shift_left | << | -| bit_shift_right | >> | -| bit_xor/bit_not | ~ | -| eq | == | -| neq | != | -| leq | <= | -| lt | < | -| geq | >= | -| gt | > | -| assign | <- | -| list_length | # | -| concat | .. | -| key_value | => | - -Notice how `-` and `~` both correspond to two ops - this is resolved by the used arity. -These symbolic aliases can also be used in an infix matter: `(+ 2 3)` is the same as (`2 + 3)`. -Additionaly, the following ops can be used as if they were n-ary: `add`,`mul`,`bit_and`,`bit_or`,`bit_xor`,`concat`. -For example, `(+ 1 2 3 4)` is the same as `(((1 + 2) + 3) + 4)`. +All other expressions are Polygolf operators. Most of them return values, but some are used for I/O and some are used for setting values in collections. +[Complete list of opcodes](docs/opcodes.generated.md). + +One can reference on opcode be either its name or its alias. Some opcodes share the alias - this is resolved by the used arity / types of inputs. +Symbolic aliases and `div`, `mod` can also be used in an infix manner: `(+ 2 3)` is the same as `(2 + 3)` and in mutating manner: `$x <- ($x + 1);` is the same as `$x +<- 1;`. + +There's an alternative syntax for indexing assignment: +`($collection @ $index) <- value;` is the same as `set_at $collection $index $value;`. + +Many text opcodes have `...[byte]`, `...[codepoint]`, `...[Ascii]` variants. Use the ascii one where possible, as that will allow target langs to choose any implementation. The other two will force implementations that are valid outside of the ascii range. ## Example diff --git a/docs/architecture.md b/docs/architecture.md index 84515ac2..484b6dd2 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -6,6 +6,8 @@ After making a change, run `npm run build` before running the cli as `node dist/ To run tests, run `npm run test`. +To see which Polygolf features are compilable to the target languages, run `npm run cover` or `npm run cover-all`. + The npm alias `npm run cli` is equivalent to `npm run build; node dist/cli.js` Some concepts (visitor, Path, etc.) are similar to those used by the JavaScript transpiler Babel, so the [Babel plugin handbook](https://github.com/jamiebuilds/babel-handbook/blob/master/translations/en/plugin-handbook.md) is worth skimming. diff --git a/docs/opcodes.generated.md b/docs/opcodes.generated.md new file mode 100644 index 00000000..fd43a2aa --- /dev/null +++ b/docs/opcodes.generated.md @@ -0,0 +1,86 @@ +# OpCodes +Hover opcode name to see a description. + +| Alias | Full name | Input | Output | +|-------|-----------|-------|--------| +| + | [add](## "Integer addition.") | [Int, Int, ...Int] | Int | +| - | [sub](## "Integer subtraction.")
[neg](## "Integer negation.") | [Int, Int]
[Int] | Int
Int | +| * | [mul](## "Integer multiplication.") | [Int, Int, ...Int] | Int | +| div | [div](## "Integer floor division.") | [Int, Int] | Int | +| ^ | [pow](## "Integer exponentiation.") | [Int, 0..oo] | Int | +| mod | [mod](## "Integer modulo (corresponds to `div`).") | [Int, Int] | Int | +| & | [bit_and](## "Integer bitwise and.") | [Int, Int, ...Int] | Int | +| \| | [bit_or](## "Integer bitwise or.") | [Int, Int, ...Int] | Int | +| ~ | [bit_xor](## "Integer bitwise xor.")
[bit_not](## "Integer bitwise not.") | [Int, Int, ...Int]
[Int] | Int
Int | +| << | [bit_shift_left](## "Integer left bitshift.") | [Int, 0..oo] | Int | +| >> | [bit_shift_right](## "Integer arithmetic right bitshift.") | [Int, 0..oo] | Int | +| gcd | [gcd](## "Greatest common divisor of two integers.") | [Int, Int, ...Int] | 1..oo | +| min | [min](## "Integer minimum.") | [Int, Int, ...Int] | Int | +| max | [max](## "Integer maximum.") | [Int, Int, ...Int] | Int | +| abs | [abs](## "Integer absolute value.") | [Int] | 0..oo | +| read[line] | [read[line]](## "Reads single line from the stdin.") | [] | Text | +| @ | [at[argv]](## "Gets argv at the 0-based `n`th position, where `n` is an integer literal.")
[at[Array]](## "Gets the item at the 0-based index.")
[at[List]](## "Gets the item at the 0-based index.")
[at_back[List]](## "Gets the item at the -1-based backwards index.")
[at[Table]](## "Gets the item at the key.")
[at[Ascii]](## "Gets the character at the 0-based index.")
[at_back[Ascii]](## "Gets the character at the -1-based backwards index.") | [0..oo]
[(Array T1 T2), T2]
[(List T1), 0..oo]
[(List T1), -oo..-1]
[(Table T1 T2), T1]
[Ascii, 0..oo]
[Ascii, -oo..-1] | Text
T1
T1
T1
T2
(Ascii 1..1)
(Ascii 1..1) | +| print | [print[Text]](## "Prints the provided argument.")
[print[Int]](## "Converts the provided argument to base 10 text and prints it.") | [Text]
[Int] | Void
Void | +| println | [println[Text]](## "Prints the provided argument followed by a \\n.")
[println[Int]](## "Converts the provided argument to base 10 text and prints it followed by a \\n.") | [Text]
[Int] | Void
Void | +| putc[byte] | [putc[byte]](## "Creates a single byte text and prints it.") | [0..255] | Void | +| putc[codepoint] | [putc[codepoint]](## "Creates a single codepoint text and prints it.") | [0..1114111] | Void | +| putc | [putc[Ascii]](## "Creates a single ascii character text and prints it.") | [0..127] | Void | +| or | [or](## "Non-shortcircuiting logical or. All arguments are to be safely evaluated in any order.") | [Bool, Bool, ...Bool] | Bool | +| and | [and](## "Non-shortcircuiting logical and. All arguments are to be safely evaluated in any order.") | [Bool, Bool, ...Bool] | Bool | +| unsafe_or | [unsafe_or](## "Shortcircuiting logical or.") | [Bool, Bool] | Bool | +| unsafe_and | [unsafe_and](## "Shortcircuiting logical and.") | [Bool, Bool] | Bool | +| not | [not](## "Logical not.") | [Bool] | Bool | +| true | [true](## "True value.") | [] | Bool | +| false | [false](## "False value.") | [] | Bool | +| < | [lt](## "Integer less than.") | [Int, Int] | Bool | +| <= | [leq](## "Integer less than or equal.") | [Int, Int] | Bool | +| >= | [geq](## "Integer greater than or equal.") | [Int, Int] | Bool | +| > | [gt](## "Integer greater than.") | [Int, Int] | Bool | +| == | [eq[Int]](## "Integer equality.")
[eq[Text]](## "Text equality.") | [Int, Int]
[Text, Text] | Bool
Bool | +| != | [neq[Int]](## "Integer inequality.")
[neq[Text]](## "Text inequality.") | [Int, Int]
[Text, Text] | Bool
Bool | +| at[byte] | [at[byte]](## "Gets the byte (as text) at the 0-based index (counting bytes).") | [Text, 0..oo] | (Text 1..1) | +| at_back[byte] | [at_back[byte]](## "Gets the byte (as text) at the -1-based backwards index (counting bytes).") | [Text, -oo..-1] | (Text 1..1) | +| at[codepoint] | [at[codepoint]](## "Gets the codepoint (as text) at the 0-based index (counting codepoints).") | [Text, 0..oo] | (Text 1..1) | +| at_back[codepoint] | [at_back[codepoint]](## "Gets the codepoint (as text) at the -1-based backwards index (counting codepoints).") | [Text, -oo..-1] | (Text 1..1) | +| set_at | [set_at[Array]](## "Sets the item at the 0-based index.")
[set_at[List]](## "Sets the item at the 0-based index.")
[set_at_back[List]](## "Sets the item at the -1-based backwards index.")
[set_at[Table]](## "Sets the item at the key.") | [(Array T1 T2), T2, T1]
[(List T1), 0..oo, T1]
[(List T1), -oo..-1, T1]
[(Table T1 T2), T1, T2] | Void
Void
Void
Void | +| slice[codepoint] | [slice[codepoint]](## "Returns a text slice that starts at the given 0-based index and has given length. Start and length are measured in codepoints.") | [Text, 0..oo, 0..oo] | Text | +| slice_back[codepoint] | [slice_back[codepoint]](## "Returns a text slice that starts at the given -1-based backwards index and has given length. Start and length are measured in codepoints.") | [Text, -oo..-1, 0..oo] | Text | +| slice[byte] | [slice[byte]](## "Returns a text slice that starts at the given 0-based index and has given length. Start and length are measured in bytes.") | [Text, 0..oo, 0..oo] | Text | +| slice_back[byte] | [slice_back[byte]](## "Returns a text slice that starts at the given -1-based backwards index and has given length. Start and length are measured in bytes.") | [Text, -oo..-1, 0..oo] | Text | +| slice | [slice[Ascii]](## "Returns a text slice that starts at the given 0-based index and has given length.")
[slice_back[Ascii]](## "Returns a text slice that starts at the given -1-based backwards index and has given length.")
[slice[List]](## "Returns a list slice that starts at the given 0-based index and has given length.")
[slice_back[List]](## "Returns a list slice that starts at the given -1-based backwards index and has given length.") | [Ascii, 0..oo, 0..oo]
[Ascii, -oo..-1, 0..oo]
[(List T1), 0..oo, 0..oo]
[(List T1), -oo..-1, 0..oo] | Ascii
Ascii
(List T1)
(List T1) | +| ord[byte] | [ord[byte]](## "Converts the byte to an integer.") | [(Text 1..1)] | 0..255 | +| ord[codepoint] | [ord[codepoint]](## "Converts the codepoint to an integer.") | [(Text 1..1)] | 0..1114111 | +| ord | [ord[Ascii]](## "Converts the character to an integer.") | [(Ascii 1..1)] | 0..127 | +| char[byte] | [char[byte]](## "Returns a byte (as text) corresponding to the integer.") | [0..255] | (Text 1..1) | +| char[codepoint] | [char[codepoint]](## "Returns a codepoint (as text) corresponding to the integer.") | [0..1114111] | (Text 1..1) | +| char | [char[Ascii]](## "Returns a character corresponding to the integer.") | [0..127] | (Ascii 1..1) | +| sorted | [sorted[Int]](## "Returns a sorted copy of the input.")
[sorted[Ascii]](## "Returns a lexicographically sorted copy of the input.") | [(List Int)]
[(List Ascii)] | (List Int)
(List Ascii) | +| reversed[byte] | [reversed[byte]](## "Returns a text in which the bytes are in reversed order.") | [Text] | Text | +| reversed[codepoint] | [reversed[codepoint]](## "Returns a text in which the codepoints are in reversed order.") | [Text] | Text | +| reversed | [reversed[Ascii]](## "Returns a text in which the characters are in reversed order.")
[reversed[List]](## "Returns a list in which the items are in reversed order.") | [Ascii]
[(List T1)] | Ascii
(List T1) | +| find[codepoint] | [find[codepoint]](## "Returns a 0-based index of the first codepoint at which the search text starts, provided it is included.") | [Text, (Text 1..oo)] | -1..oo | +| find[byte] | [find[byte]](## "Returns a 0-based index of the first byte at which the search text starts, provided it is included.") | [Text, (Text 1..oo)] | -1..oo | +| find | [find[Ascii]](## "Returns a 0-based index of the first character at which the search text starts, provided it is included.")
[find[List]](## "Returns a 0-based index of the first occurence of the searched item, provided it is included.") | [Ascii, Ascii]
[(List T1), T1] | -1..oo
-1..2147483647 | +| contains | [contains[Array]](## "Asserts whether an item is included in the array.")
[contains[List]](## "Asserts whether an item is included in the list.")
[contains[Table]](## "Asserts whether an item is included in the keys of the table.")
[contains[Set]](## "Asserts whether an item is included in the set.")
[contains[Text]](## "Asserts whether the 2nd argument is a substring of the 1st one.") | [(Array T1 T2), T1]
[(List T1), T1]
[(Table T1 T2), T1]
[(Set T1), T1]
[Text, Text] | Bool
Bool
Bool
Bool
Bool | +| # | [size[List]](## "Returns the length of the list.")
[size[Set]](## "Returns the cardinality of the set.")
[size[Table]](## "Returns the number of keys in the table.")
[size[Ascii]](## "Returns the length of the text.") | [(List T1)]
[(Set T1)]
[(Table T1 T2)]
[Ascii] | 0..2147483647
0..2147483647
0..2147483647
0..oo | +| size[codepoint] | [size[codepoint]](## "Returns the length of the text in codepoints.") | [Text] | 0..oo | +| size[byte] | [size[byte]](## "Returns the length of the text in bytes.") | [Text] | 0..2147483648 | +| include | [include](## "Modifies the set by including the given item.") | [(Set T1), T1] | Void | +| push | [push](## "Modifies the list by pushing the given item at the end.") | [(List T1), T1] | Void | +| .. | [append](## "Returns a new list with the given item appended at the end.")
[concat[List]](## "Returns a new list formed by concatenation of the inputs.")
[concat[Text]](## "Returns a new text formed by concatenation of the inputs.") | [(List T1), T1]
[...(List T1)]
[...Text] | (List T1)
?
? | +| repeat | [repeat](## "Repeats the text a given amount of times.") | [Text, 0..oo] | Text | +| split | [split](## "Splits the text by the delimiter.") | [Text, Text] | (List Text) | +| split_whitespace | [split_whitespace](## "Splits the text by any whitespace.") | [Text] | (List Text) | +| join | [join](## "Joins the items using the delimiter.") | [(List Text), Text] | Text | +| right_align | [right_align](## "Right-aligns the text using spaces to a minimum length.") | [Text, 0..oo] | Text | +| replace | [replace](## "Replaces all occurences of a given text with another text.") | [Text, (Text 1..oo), Text] | Text | +| starts_with | [starts_with](## "Checks whether the second argument is a prefix of the first.") | [Text, Text] | Bool | +| ends_with | [ends_with](## "Checks whether the second argument is a suffix of the first.") | [Text, Text] | Bool | +| int_to_bin_aligned | [int_to_bin_aligned](## "Converts the integer to a 2-base text and alignes to a minimum length.") | [0..oo, 0..oo] | Ascii | +| int_to_hex_aligned | [int_to_hex_aligned](## "Converts the integer to a 16-base text and alignes to a minimum length.") | [0..oo, 0..oo] | Ascii | +| int_to_dec | [int_to_dec](## "Converts the integer to a 10-base text.") | [Int] | (Ascii 1..oo) | +| int_to_bin | [int_to_bin](## "Converts the integer to a 2-base text.") | [0..oo] | (Ascii 1..oo) | +| int_to_hex | [int_to_hex](## "Converts the integer to a 16-base text.") | [0..oo] | (Ascii 1..oo) | +| int_to_bool | [int_to_bool](## "Converts 0 to false and 1 to true.") | [0..1] | Bool | +| dec_to_int | [dec_to_int](## "Parses a integer from a 10-base text.") | [Ascii] | Int | +| bool_to_int | [bool_to_int](## "Converts false to 0 and true to 1.") | [Bool] | 0..1 | diff --git a/package-lock.json b/package-lock.json index e70398c2..9b607edb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "license": "MIT", "dependencies": { "@datastructures-js/priority-queue": "^6.3.0", + "as-table": "^1.0.55", "moo": "^0.5.2", "nearley": "^2.20.1", "yargs": "^17.6.0" @@ -2215,6 +2216,14 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/as-table": { + "version": "1.0.55", + "resolved": "https://registry.npmjs.org/as-table/-/as-table-1.0.55.tgz", + "integrity": "sha512-xvsWESUJn0JN421Xb9MQw6AsMHRCUknCe0Wjlxvjud80mU4E6hQf1A6NzQKcYNmYw62MfzEtXc+badstZP3JpQ==", + "dependencies": { + "printable-characters": "^1.0.42" + } + }, "node_modules/available-typed-arrays": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz", @@ -5669,6 +5678,11 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/printable-characters": { + "version": "1.0.42", + "resolved": "https://registry.npmjs.org/printable-characters/-/printable-characters-1.0.42.tgz", + "integrity": "sha512-dKp+C4iXWK4vVYZmYSd0KBH5F/h1HoZRsbJ82AVKRO3PEo8L4lBS/vLwhVtpwwuYcoIsVY+1JYKR268yn480uQ==" + }, "node_modules/prompts": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", diff --git a/package.json b/package.json index 2ba586d9..0d77eda4 100644 --- a/package.json +++ b/package.json @@ -7,18 +7,21 @@ "polygolf": "./dist/cli.js" }, "scripts": { - "prettier": "prettier --embedded-language-formatting=off --write .", + "prettier": "prettier --write .", "eslint-fix": "eslint --fix \"src/**\"", - "build": "npm run build-nearley && etsc", + "build": "npm run build-nearley && etsc && npm run build-docs", "build-nearley": "nearleyc src/frontend/grammar.ne -o src/frontend/grammar.ts", + "build-docs": "node \"dist/docs-gen/index.js\"", "test:typecheck": "npm run build-nearley && tsc --noEmit", - "test:formatting": "prettier-check --embedded-language-formatting=off .", + "test:formatting": "prettier-check .", "test:lint": "eslint \"src/**\"", "test:jest": "jest --config jest.config.js", "test": "npm run test:formatting && npm run test:typecheck && npm run test:lint && npm run test:build && npm run test:jest", "cli": "npm run build && node --enable-source-maps dist/cli.js", "cli-debug": "npm run build && node --inspect-brk dist/cli.js", - "test:build": "npm run build && node \"dist/markdown-tests/build.js\"" + "test:build": "npm run build && node \"dist/markdown-tests/build.js\"", + "cover": "npm run build && node \"dist/cover/index.js\"", + "cover-all": "npm run build && node \"dist/cover/index.js\" -a" }, "repository": { "type": "git", @@ -48,9 +51,10 @@ "typescript": "^5.2.2" }, "dependencies": { + "@datastructures-js/priority-queue": "^6.3.0", + "as-table": "^1.0.55", "moo": "^0.5.2", "nearley": "^2.20.1", - "yargs": "^17.6.0", - "@datastructures-js/priority-queue": "^6.3.0" + "yargs": "^17.6.0" } } diff --git a/src/IR/assignments.ts b/src/IR/assignments.ts index ad8735eb..b9d4baba 100644 --- a/src/IR/assignments.ts +++ b/src/IR/assignments.ts @@ -127,8 +127,10 @@ export function assignment(variable: LValue | string, expr: Node): Assignment { export function isAssignment(x: Node): x is Assignment { return x.kind === "Assignment"; } -export function isAssignmentToIdentifier(x: Node): x is Assignment { - return isAssignment(x) && isIdent()(x.variable); +export function isAssignmentToIdent( + ...names: (Name | Identifier)[] +): (x: Node) => x is Assignment> { + return ((x: Node) => isAssignment(x) && isIdent(...names)(x.variable)) as any; } export function manyToManyAssignment( diff --git a/src/IR/exprs.ts b/src/IR/exprs.ts index 0217235c..66405f3a 100644 --- a/src/IR/exprs.ts +++ b/src/IR/exprs.ts @@ -1,3 +1,4 @@ +import { byteLength, charLength } from "../common/strings"; import { getArithmeticType } from "../common/getType"; import { stringify } from "../common/stringify"; import { @@ -10,22 +11,26 @@ import { type Node, type Integer, type MutatingInfix, - isCommutative, int, isAssociative, text, integerType, - type AliasedOpCode, - type FrontendOpCode, - type AssociativeOpCode, - type CommutativeOpCode, isConstantType, isBinary, booleanNotOpCode, type Text, type IDCastable, castID, + type VariadicOpCode, + isCommutative, + isOpCode, + inverseOpCode, + type OpCodeArgValues, + isUnary, + opCodeDefinitions, + isNullary, } from "./IR"; +import { mapObjectValues } from "../common/arrays"; export interface ImplicitConversion extends BaseNode { readonly kind: "ImplicitConversion"; @@ -53,7 +58,7 @@ export interface ImplicitConversion extends BaseNode { export interface Op extends BaseNode { readonly kind: "Op"; readonly op: Op; - readonly args: readonly Node[]; + readonly args: OpCodeArgValues; } export interface KeyValue extends BaseNode { @@ -99,7 +104,6 @@ export interface IndexCall extends BaseNode { readonly kind: "IndexCall"; readonly collection: Node; readonly index: Node; - readonly oneIndexed: boolean; } export interface RangeIndexCall extends BaseNode { @@ -108,7 +112,6 @@ export interface RangeIndexCall extends BaseNode { readonly low: Node; readonly high: Node; readonly step: Node; - readonly oneIndexed: boolean; } export interface Infix extends BaseNode { @@ -175,6 +178,29 @@ export function keyValue(key: Node, value: Node): KeyValue { }; } +/** + * This object contains contructors for each opcode, with signatures + * validating the arities. + */ +export const op = { + ...(mapObjectValues( + opCodeDefinitions, + (v, k) => + isNullary(k) + ? opUnsafe(k) + : (...x: Node[]) => + opUnsafe( + k, + ...x.filter((x) => typeof x === "object" && "kind" in x), + ), // allow unary opcodes to be used in map + ) as { + [O in OpCode]: OpCodeArgValues extends readonly [] + ? Op + : (...args: OpCodeArgValues) => Op; + }), + unsafe: opUnsafe, +} as const; + /** * This assumes that the construction will not break the invariants described * on `Op` interface and hence is made private. @@ -187,27 +213,47 @@ function _op(op: OpCode, ...args: Node[]): Op { }; } -export function op(opCode: OpCode, ...args: Node[]): Node { +/** + * This is the implementation respecting the invariants described on the `Op` + * interface, but it doesn't validate arity. + */ +function opUnsafe(opCode: OpCode, ...args: Node[]): Node { + if (!isOpCode(opCode)) return _op(opCode, ...args); + if (isUnary(opCode)) { + const value = evalUnary(opCode, args[0]); + if (value !== null) return value; + } if (opCode === "not" || opCode === "bit_not") { const arg = args[0]; if (isOp()(arg)) { - if (arg.op === opCode) return arg.args[0]; + if (arg.op === opCode && arg.args[0]?.kind !== "ImplicitConversion") + return arg.args[0]!; if (opCode === "not") { - const negated = booleanNotOpCode(arg.op as BinaryOpCode); - if (negated != null) { - return op(negated, arg.args[0], arg.args[1]); + if (arg.op in booleanNotOpCode) { + return op.unsafe( + booleanNotOpCode[arg.op as keyof typeof booleanNotOpCode], + arg.args[0]!, + arg.args[1]!, + ); } } } } + if ( + opCode in inverseOpCode && + isOp(inverseOpCode[opCode as keyof typeof inverseOpCode])(args[0]) && + args[0].args[0].kind !== "ImplicitConversion" + ) { + return args[0].args[0]; + } if (opCode === "neg") { - if (isIntLiteral()(args[0])) { + if (isInt()(args[0])) { return int(-args[0].value); } - return op("mul", int(-1), args[0]); + return op.mul(int(-1), args[0]); } if (opCode === "sub") { - return op("add", args[0], op("neg", args[1])); + return op.add(args[0], op.neg(args[1])); } if (isAssociative(opCode)) { args = args.flatMap((x) => (isOp(opCode)(x) ? x.args : [x])); @@ -215,8 +261,8 @@ export function op(opCode: OpCode, ...args: Node[]): Node { else { if (isCommutative(opCode)) { args = args - .filter((x) => isIntLiteral()(x)) - .concat(args.filter((x) => !isIntLiteral()(x))); + .filter((x) => isInt()(x)) + .concat(args.filter((x) => !isInt()(x))); } else { args = args.filter((x) => !isText("")(x)); if ( @@ -229,7 +275,7 @@ export function op(opCode: OpCode, ...args: Node[]): Node { const newArgs: Node[] = []; for (const arg of args) { if (newArgs.length > 0) { - const combined = evalInfix(opCode, newArgs[newArgs.length - 1], arg); + const combined = evalBinary(opCode, newArgs[newArgs.length - 1], arg); if (combined !== null) { newArgs[newArgs.length - 1] = combined; } else { @@ -244,10 +290,10 @@ export function op(opCode: OpCode, ...args: Node[]): Node { ); if (toNegate !== undefined) { args = args.map((x) => - isIntLiteral()(x) + isInt()(x) ? int(-x.value) : x === toNegate - ? op("add", ...(x as Op).args.map((y) => op("neg", y))) + ? op.unsafe("add", ...(x as Op).args.map(op.neg)) : x, ); } @@ -256,7 +302,7 @@ export function op(opCode: OpCode, ...args: Node[]): Node { if ( opCode === "mul" && args.length > 1 && - isIntLiteral(1n)(args[0]) && + isInt(1n)(args[0]) && args[1].kind !== "ImplicitConversion" ) { args = args.slice(1); @@ -265,10 +311,10 @@ export function op(opCode: OpCode, ...args: Node[]): Node { if (args.length === 1) return args[0]; } if (isBinary(opCode) && args.length === 2) { - const combined = evalInfix(opCode, args[0], args[1]); + const combined = evalBinary(opCode, args[0], args[1]); if ( combined !== null && - (!isIntLiteral()(combined) || + (!isInt()(combined) || opCode !== "pow" || // only eval pow if it is a low number (combined.value < 1000 && combined.value > -1000)) ) { @@ -278,11 +324,15 @@ export function op(opCode: OpCode, ...args: Node[]): Node { return _op(opCode, ...args); } -function evalInfix(op: BinaryOpCode, left: Node, right: Node): Node | null { - if (op === "concat" && isText()(left) && isText()(right)) { +function evalBinary( + op: BinaryOpCode | VariadicOpCode, + left: Node, + right: Node, +): Integer | Text | null { + if (op === "concat[Text]" && isText()(left) && isText()(right)) { return text(left.value + right.value); } - if (isIntLiteral()(left) && isIntLiteral()(right)) { + if (isInt()(left) && isInt()(right)) { try { const type = getArithmeticType( op, @@ -297,6 +347,20 @@ function evalInfix(op: BinaryOpCode, left: Node, right: Node): Node | null { return null; } +function evalUnary(op: UnaryOpCode, arg: Node): Integer | Text | null { + if (isText()(arg)) { + const value = arg.value; + switch (op) { + case "size[byte]": + case "size[Ascii]": + return int(byteLength(value)); + case "size[codepoint]": + return int(charLength(value)); + } + } + return null; +} + /** Simplifies a polynomial represented as an array of terms. */ function simplifyPolynomial(terms: Node[]): Node[] { const coeffMap = new Map(); @@ -312,9 +376,9 @@ function simplifyPolynomial(terms: Node[]): Node[] { } } for (const x of terms) { - if (isIntLiteral()(x)) constant += x.value; + if (isInt()(x)) constant += x.value; else if (isOp("mul")(x)) { - if (isIntLiteral()(x.args[0])) add(x.args[0].value, x.args.slice(1)); + if (isInt()(x.args[0])) add(x.args[0].value, x.args.slice(1)); else add(1n, x.args); } else add(1n, [x]); } @@ -337,8 +401,8 @@ function simplifyPolynomial(terms: Node[]): Node[] { return result; } -export const add1 = (expr: Node) => op("add", expr, int(1n)); -export const sub1 = (expr: Node) => op("add", expr, int(-1n)); +export const succ = (expr: Node) => op.add(expr, int(1n)); +export const prec = (expr: Node) => op.add(expr, int(-1n)); export function functionCall( func: string | Node, @@ -386,16 +450,11 @@ export function propertyCall( }; } -export function indexCall( - collection: string | Node, - index: Node, - oneIndexed: boolean = false, -): IndexCall { +export function indexCall(collection: string | Node, index: Node): IndexCall { return { kind: "IndexCall", collection: typeof collection === "string" ? id(collection) : collection, index, - oneIndexed, }; } @@ -404,7 +463,6 @@ export function rangeIndexCall( low: Node, high: Node, step: Node, - oneIndexed: boolean = false, ): RangeIndexCall { return { kind: "RangeIndexCall", @@ -412,7 +470,6 @@ export function rangeIndexCall( low, high, step, - oneIndexed, }; } @@ -473,7 +530,7 @@ export function namedArg(name: string, value: T): NamedArg { } export function print(value: Node, newline: boolean = true): Node { - return op(newline ? "println" : "print", value); + return op[newline ? "println[Text]" : "print[Text]"](value); } export function getArgs( @@ -556,7 +613,7 @@ export function isUserIdent( ))) as any; } -export function isIntLiteral( +export function isInt( ...vals: Value[] ): (x: Node) => x is Integer { return ((x: Node) => @@ -565,7 +622,7 @@ export function isIntLiteral( } export function isNegativeLiteral(expr: Node) { - return isIntLiteral()(expr) && expr.value < 0n; + return isInt()(expr) && expr.value < 0n; } /** @@ -578,35 +635,7 @@ export function isNegative(expr: Node) { ); } -export function isOp( - ...ops: O[] -): (x: Node) => x is Op< - // Typesafe-wise, this is the same as `x is Op`. - // However, this allows `O` to be written using the type aliases. - // Alias using the first type that is a match (that is a subtype) and union the rest. - // For some reason, when I alias this type, it no longer works. - AliasedOpCode< - O, - OpCode, - AliasedOpCode< - O, - FrontendOpCode, - AliasedOpCode< - O, - BinaryOpCode, - AliasedOpCode< - O, - UnaryOpCode, - AliasedOpCode< - O, - AssociativeOpCode, - AliasedOpCode - > - > - > - > - > -> { +export function isOp(...ops: O[]): (x: Node) => x is Op { return ((x: Node) => x.kind === "Op" && (ops.length === 0 || ops?.includes(x.op as any))) as any; } diff --git a/src/IR/opcodes.ts b/src/IR/opcodes.ts index 69237fbe..f2fe296d 100644 --- a/src/IR/opcodes.ts +++ b/src/IR/opcodes.ts @@ -1,316 +1,581 @@ -export const FrontendOpCodes = [ - "add", - "sub", - "mul", - "div", - "mod", - "pow", - "bit_and", - "bit_or", - "bit_xor", - "min", - "max", - "lt", - "leq", - "eq", - "neq", - "gt", - "geq", - "or", - "and", - "array_contains", - "list_contains", - "table_contains_key", - "set_contains", - "array_get", - "list_get", - "table_get", - "list_push", - "concat", - "repeat", - "text_contains", - "text_byte_find", - "text_codepoint_find", - "text_split", - "text_get_byte", - "text_get_byte_slice", - "text_get_codepoint", - "text_get_codepoint_slice", - "join", - "right_align", - "int_to_bin_aligned", - "int_to_hex_aligned", - "simplify_fraction", - - "abs", - "bit_not", - "neg", - "not", - "int_to_text", - "int_to_bin", - "int_to_hex", - "text_to_int", - "bool_to_int", - "int_to_text_byte", // Returns a single byte text using the specified byte. - "int_to_codepoint", // Returns a single codepoint text using the specified integer. - "list_length", - "text_byte_length", // Returns the text length in bytes. - "text_codepoint_length", // Returns the text length in codepoints. - "text_split_whitespace", - "text_byte_reversed", // Returns a text containing the reversed order of bytes. - "text_codepoint_reversed", // Returns a text containing the reversed order of codepoints. - "text_byte_to_int", - "codepoint_to_int", - "read_line", - "true", - "false", - "print", - "println", - "print_int", - "println_int", - "text_replace", - "array_set", - "list_set", - "table_set", - "sorted", - "argv_get", -] as const; - -// It may seem that the `string &` is redundant, but the `isPolygolf` typeguard doesn't work without it. -export type FrontendOpCode = string & (typeof FrontendOpCodes)[number]; - -export function isFrontend(op: OpCode): op is FrontendOpCode { - return FrontendOpCodes.includes(op as any); -} +import type { Node } from "./IR"; +import { + type Type, + typeArg, + textType as text, + listType as list, + arrayType as array, + booleanType as bool, + integerType as int, + setType as set, + tableType as table, + asciiType as ascii, +} from "./types"; -export const UnaryOpCodes = [ - "print", - "println", - "print_int", - "println_int", - "putc", - "argv_get", - "abs", - "bit_not", - "neg", - "not", - "int_to_text", - "int_to_bin", - "int_to_hex", - "int_to_bool", - "text_to_int", - "bool_to_int", - "int_to_text_byte", - "int_to_codepoint", - "list_length", - "text_codepoint_length", - "text_byte_length", - "text_split_whitespace", - "sorted", - "text_byte_reversed", - "text_codepoint_reversed", - "text_byte_to_int", // (text_byte_to_int (text_get_byte $x $i)) is equivalent to (text_get_byte_to_int $x $i), "text_byte_to_int" is the inverse of "int_to_text_byte" - "codepoint_to_int", // (codepoint_to_int (text_get_codepoint $x $i)) is equivalent to (text_get_codepoint_to_int $x $i), "codepoint_to_int" is the inverse of "int_to_codepoint" -] as const; -export type UnaryOpCode = string & (typeof UnaryOpCodes)[number]; +interface OpCodeDefinition { + args: AnyOpCodeArgTypes; + front?: true | string; + assoc?: true; + commutes?: true; +} -export function isUnary(op: OpCode): op is UnaryOpCode { - return UnaryOpCodes.includes(op as any); +export interface Rest { + rest: T; +} +export type AnyOpCodeArgTypes = + | readonly [...(readonly Type[])] + | readonly [...(readonly Type[]), Rest]; +function rest(rest: T): Rest { + return { rest }; } -export const CommutativeOpCodes = [ - "add", - "mul", - "bit_and", - "bit_or", - "bit_xor", - "and", - "or", - "gcd", - "min", - "max", -] as const; - -export type CommutativeOpCode = string & (typeof CommutativeOpCodes)[number]; +const T1 = typeArg("T1"); +const T2 = typeArg("T2"); -export function isCommutative(op: OpCode): op is CommutativeOpCode { - return CommutativeOpCodes.includes(op as any); -} +const int2OrMore = [int(), int(), rest(int())] as const; +const bool2OrMore = [bool, bool, rest(bool)] as const; +export const opCodeDefinitions = { + // Arithmetic + add: { args: int2OrMore, front: "+", assoc: true, commutes: true }, + sub: { args: [int(), int()], front: "-" }, + mul: { args: int2OrMore, front: "*", assoc: true, commutes: true }, + div: { args: [int(), int()], front: "div" }, + trunc_div: { args: [int(), int()] }, + unsigned_trunc_div: { args: [int(), int()] }, + pow: { args: [int(), int(0)], front: "^" }, + mod: { args: [int(), int()], front: "mod" }, + rem: { args: [int(), int()] }, + unsigned_rem: { args: [int(), int()] }, + bit_and: { args: int2OrMore, front: "&", assoc: true, commutes: true }, + bit_or: { args: int2OrMore, front: "|", assoc: true, commutes: true }, + bit_xor: { args: int2OrMore, front: "~", assoc: true, commutes: true }, + bit_shift_left: { args: [int(), int(0)], front: "<<" }, + bit_shift_right: { args: [int(), int(0)], front: ">>" }, + gcd: { args: int2OrMore, front: true, assoc: true, commutes: true }, + min: { args: int2OrMore, front: true, assoc: true, commutes: true }, + max: { args: int2OrMore, front: true, assoc: true, commutes: true }, + neg: { args: [int()], front: "-" }, + abs: { args: [int()], front: true }, + bit_not: { args: [int()], front: "~" }, -export const AssociativeOpCodes = [...CommutativeOpCodes, "concat"] as const; + // Input + "read[codepoint]": { args: [] }, + "read[byte]": { args: [] }, + "read[Int]": { args: [] }, + "read[line]": { args: [], front: true }, + "at[argv]": { args: [int(0)], front: "@" }, + argv: { args: [] }, + argc: { args: [] }, -export type AssociativeOpCode = string & (typeof AssociativeOpCodes)[number]; + // Output + "print[Text]": { args: [text()], front: "print" }, + "print[Int]": { args: [int()], front: "print" }, + "println[Text]": { args: [text()], front: "println" }, + "println[Int]": { args: [int()], front: "println" }, + println_list_joined: { args: [list(text()), text()] }, + println_many_joined: { args: [text(), text(), rest(text())] }, + "putc[byte]": { args: [int(0, 255)], front: true }, + "putc[codepoint]": { args: [int(0, 0x10ffff)], front: true }, + "putc[Ascii]": { args: [int(0, 127)], front: "putc" }, -export function isAssociative(op: OpCode): op is AssociativeOpCode { - return AssociativeOpCodes.includes(op as any); -} + // Bool arithmetic + or: { args: bool2OrMore, front: true, assoc: true, commutes: true }, + and: { args: bool2OrMore, front: true, assoc: true, commutes: true }, + unsafe_or: { args: [bool, bool], front: true, assoc: true }, + unsafe_and: { args: [bool, bool], front: true }, + not: { args: [bool], front: true }, + true: { args: [], front: true }, + false: { args: [], front: true }, -export const BinaryOpCodes = [ - // (num, num) => num - "add", - "sub", - "mul", - "div", - "trunc_div", - "unsigned_trunc_div", - "pow", - "mod", - "rem", - "unsigned_rem", - "bit_and", - "bit_or", - "bit_xor", - "bit_shift_left", - "bit_shift_right", - "gcd", - "min", - "max", - // (num, num) => bool - "lt", - "leq", - "eq", - "neq", - "geq", - "gt", - // (bool, bool) => bool - "or", - "and", - "unsafe_or", - "unsafe_and", - // membership - "array_contains", - "list_contains", - "table_contains_key", - "set_contains", - // collection get - "array_get", - "list_get", - "table_get", - // other - "println_list_joined", - "list_push", - "list_find", // returns the 0-index of the first occurence of or -1 if it is not found - "concat", - "repeat", - "text_contains", - "text_codepoint_find", // (text_codepoint_find a b) returns the codepoint-0-index of the start of the first occurence of b in a or -1 if it is not found - "text_byte_find", // (text_byte_find a b) returns the byte-0-index of the start of the first occurence of b in a or -1 if it is not found - "text_split", - "text_get_byte", // returns a single byte text at the specified byte-0-index - "text_get_codepoint", // returns a single codepoint text at the specified codepoint-0-index - "text_get_codepoint_to_int", // gets the codepoint at the specified codepoint-0-index as an integer - "text_get_byte_to_int", // gets the byte at the specified byte-0-index as an integer - "join", - "right_align", - "int_to_bin_aligned", // Converts the given integer to text representing the value in binary. The result is aligned with 0s to the specified number of places. - "int_to_hex_aligned", // Converts the given integer to text representing the value in hexadecimal. The result is aligned with 0s to the specified number of places. - "simplify_fraction", // Given two integers, p,q, returns a text representation of the reduced version of the fraction p/q. -] as const; - -export type BinaryOpCode = string & (typeof BinaryOpCodes)[number]; + // Comparison + lt: { args: [int(), int()], front: "<" }, + leq: { args: [int(), int()], front: "<=" }, + geq: { args: [int(), int()], front: ">=" }, + gt: { args: [int(), int()], front: ">" }, + "eq[Int]": { args: [int(), int()], front: "==", commutes: true }, + "eq[Text]": { args: [text(), text()], front: "==", commutes: true }, + "neq[Int]": { args: [int(), int()], front: "!=", commutes: true }, + "neq[Text]": { args: [text(), text()], front: "!=", commutes: true }, + + // Access members + "at[Array]": { args: [array(T1, T2), T2], front: "@" }, + "at[List]": { args: [list(T1), int(0)], front: "@" }, + "at_back[List]": { args: [list(T1), int("-oo", -1)], front: "@" }, + "at[Table]": { args: [table(T1, T2), T1], front: "@" }, + "at[Ascii]": { args: [ascii, int(0)], front: "@" }, + "at_back[Ascii]": { args: [ascii, int("-oo", -1)], front: "@" }, + "at[byte]": { args: [text(), int(0)], front: true }, + "at_back[byte]": { args: [text(), int("-oo", -1)], front: true }, + "at[codepoint]": { args: [text(), int(0)], front: true }, + "at_back[codepoint]": { args: [text(), int("-oo", -1)], front: true }, + "set_at[Array]": { args: [array(T1, T2), T2, T1], front: "set_at" }, + "set_at[List]": { args: [list(T1), int(0), T1], front: "set_at" }, + "set_at_back[List]": { + args: [list(T1), int("-oo", -1), T1], + front: "set_at", + }, + "set_at[Table]": { args: [table(T1, T2), T1, T2], front: "set_at" }, + + // Slice + "slice[codepoint]": { args: [text(), int(0), int(0)], front: true }, + "slice_back[codepoint]": { + args: [text(), int("-oo", -1), int(0)], + front: true, + }, + "slice[byte]": { args: [text(), int(0), int(0)], front: true }, + "slice_back[byte]": { args: [text(), int("-oo", -1), int(0)], front: true }, + "slice[Ascii]": { args: [ascii, int(0), int(0)], front: "slice" }, + "slice_back[Ascii]": { + args: [ascii, int("-oo", -1), int(0)], + front: "slice", + }, + "slice[List]": { args: [list(T1), int(0), int(0)], front: "slice" }, + "slice_back[List]": { + args: [list(T1), int("-oo", -1), int(0)], + front: "slice", + }, + + // Chars + "ord_at[byte]": { args: [text(), int(0)] }, + "ord_at_back[byte]": { args: [text(), int("-oo", -1)] }, + "ord_at[codepoint]": { args: [text(), int(0)] }, + "ord_at_back[codepoint]": { args: [text(), int("-oo", -1)] }, + "ord_at[Ascii]": { args: [ascii, int(0)] }, + "ord_at_back[Ascii]": { args: [ascii, int("-oo", -1)] }, + "ord[byte]": { args: [text(int(1, 1))], front: true }, + "ord[codepoint]": { args: [text(int(1, 1))], front: true }, + "ord[Ascii]": { args: [text(int(1, 1), true)], front: "ord" }, + "char[byte]": { args: [int(0, 255)], front: true }, + "char[codepoint]": { args: [int(0, 0x10ffff)], front: true }, + "char[Ascii]": { args: [int(0, 127)], front: "char" }, + + // Order + "sorted[Int]": { args: [list(int())], front: "sorted" }, + "sorted[Ascii]": { args: [list(ascii)], front: "sorted" }, + "reversed[byte]": { args: [text()], front: true }, + "reversed[codepoint]": { args: [text()], front: true }, + "reversed[Ascii]": { args: [ascii], front: "reversed" }, + "reversed[List]": { args: [list(T1)], front: "reversed" }, + "find[codepoint]": { args: [text(), text(int(1))], front: true }, + "find[byte]": { args: [text(), text(int(1))], front: true }, + "find[Ascii]": { args: [ascii, ascii], front: "find" }, + "find[List]": { args: [list(T1), T1], front: "find" }, + + // Membership + "contains[Array]": { args: [array(T1, T2), T1], front: "contains" }, + "contains[List]": { args: [list(T1), T1], front: "contains" }, + "contains[Table]": { args: [table(T1, T2), T1], front: "contains" }, + "contains[Set]": { args: [set(T1), T1], front: "contains" }, + "contains[Text]": { args: [text(), text()], front: "contains" }, + + // Size + "size[List]": { args: [list(T1)], front: "#" }, + "size[Set]": { args: [set(T1)], front: "#" }, + "size[Table]": { args: [table(T1, T2)], front: "#" }, + "size[Ascii]": { args: [ascii], front: "#" }, + "size[codepoint]": { args: [text()], front: true }, + "size[byte]": { args: [text()], front: true }, + + // Adding items + include: { args: [set(T1), T1], front: true }, + push: { args: [list(T1), T1], front: true }, + append: { args: [list(T1), T1], front: ".." }, + "concat[List]": { args: [rest(list(T1))], front: "..", assoc: true }, + "concat[Text]": { args: [rest(text())], front: "..", assoc: true }, + + // Text ops + repeat: { args: [text(), int(0)], front: true }, + split: { args: [text(), text()], front: true }, + split_whitespace: { args: [text()], front: true }, + join: { args: [list(text()), text()], front: true }, + right_align: { args: [text(), int(0)], front: true }, + replace: { args: [text(), text(int(1)), text()], front: true }, + text_multireplace: { args: [text(), text(), rest(text())] }, + starts_with: { args: [text(), text()], front: true }, + ends_with: { args: [text(), text()], front: true }, + + // Text / Bool <-> Int + int_to_bin_aligned: { args: [int(0), int(0)], front: true }, + int_to_hex_aligned: { args: [int(0), int(0)], front: true }, + int_to_dec: { args: [int()], front: true }, + int_to_bin: { args: [int(0)], front: true }, + int_to_hex: { args: [int(0)], front: true }, + int_to_bool: { args: [int(0, 1)], front: true }, + dec_to_int: { args: [ascii], front: true }, + bool_to_int: { args: [bool], front: true }, +} as const satisfies Record; + +type AnyOpCode = keyof typeof opCodeDefinitions; + +export type OpCodeArgTypes = + (typeof opCodeDefinitions)[T]["args"]; + +type ValuesOfLengthOf = { + [K in keyof T]: Node; +}; + +export type OpCodeArgValues< + O extends OpCode = OpCode, + Types extends OpCodeArgTypes = OpCodeArgTypes, +> = Types extends readonly [...infer T, Rest] + ? [...ValuesOfLengthOf, ...(readonly Node[])] + : ValuesOfLengthOf; + +export const opCodeDescriptions: Record = { + add: "Integer addition.", + sub: "Integer subtraction.", + mul: "Integer multiplication.", + div: "Integer floor division.", + trunc_div: "Integer truncating (towards zero) division.", + unsigned_trunc_div: + "Integer truncating division treating the operands as unsigned.", + pow: "Integer exponentiation.", + mod: "Integer modulo (corresponds to `div`).", + rem: "Integer remainder (corresponds to `trunc_div`).", + unsigned_rem: + "Integer unsigned remainder (corresponds to `unsigned_trunc_div`).", + bit_and: "Integer bitwise and.", + bit_or: "Integer bitwise or.", + bit_xor: "Integer bitwise xor.", + bit_shift_left: "Integer left bitshift.", + bit_shift_right: "Integer arithmetic right bitshift.", + gcd: "Greatest common divisor of two integers.", + min: "Integer minimum.", + max: "Integer maximum.", + neg: "Integer negation.", + abs: "Integer absolute value.", + bit_not: "Integer bitwise not.", + + // Input + "read[codepoint]": "Reads single codepoint from the stdin.", + "read[byte]": "Reads single byte from the stdin.", + "read[Int]": "Reads single signed integer from the stdin.", + "read[line]": "Reads single line from the stdin.", + "at[argv]": + "Gets argv at the 0-based `n`th position, where `n` is an integer literal.", + argv: "Gets argv as a list.", + argc: "Gets the length of argv.", + + // Output + "print[Text]": "Prints the provided argument.", + "print[Int]": "Converts the provided argument to base 10 text and prints it.", + "println[Text]": "Prints the provided argument followed by a \\n.", + "println[Int]": + "Converts the provided argument to base 10 text and prints it followed by a \\n.", + println_list_joined: + "Joins the items in the list using the delimiter and prints the result.", + println_many_joined: + "Joins the items in the list using the delimiter and prints the result.", + "putc[byte]": "Creates a single byte text and prints it.", + "putc[codepoint]": "Creates a single codepoint text and prints it.", + "putc[Ascii]": "Creates a single ascii character text and prints it.", + + // Bool arithmetic + or: "Non-shortcircuiting logical or. All arguments are to be safely evaluated in any order.", + and: "Non-shortcircuiting logical and. All arguments are to be safely evaluated in any order.", + unsafe_or: "Shortcircuiting logical or.", + unsafe_and: "Shortcircuiting logical and.", + not: "Logical not.", + true: "True value.", + false: "False value.", + + // Comparison + lt: "Integer less than.", + leq: "Integer less than or equal.", + geq: "Integer greater than or equal.", + gt: "Integer greater than.", + "eq[Int]": "Integer equality.", + "eq[Text]": "Text equality.", + "neq[Int]": "Integer inequality.", + "neq[Text]": "Text inequality.", + + // Access members + "at[Array]": "Gets the item at the 0-based index.", + "at[List]": "Gets the item at the 0-based index.", + "at_back[List]": "Gets the item at the -1-based backwards index.", + "at[Table]": "Gets the item at the key.", + "at[Ascii]": "Gets the character at the 0-based index.", + "at_back[Ascii]": "Gets the character at the -1-based backwards index.", + "at[byte]": "Gets the byte (as text) at the 0-based index (counting bytes).", + "at_back[byte]": + "Gets the byte (as text) at the -1-based backwards index (counting bytes).", + "at[codepoint]": + "Gets the codepoint (as text) at the 0-based index (counting codepoints).", + "at_back[codepoint]": + "Gets the codepoint (as text) at the -1-based backwards index (counting codepoints).", + "set_at[Array]": "Sets the item at the 0-based index.", + "set_at[List]": "Sets the item at the 0-based index.", + "set_at_back[List]": "Sets the item at the -1-based backwards index.", + "set_at[Table]": "Sets the item at the key.", + + // Slice + "slice[codepoint]": + "Returns a text slice that starts at the given 0-based index and has given length. Start and length are measured in codepoints.", + "slice_back[codepoint]": + "Returns a text slice that starts at the given -1-based backwards index and has given length. Start and length are measured in codepoints.", + "slice[byte]": + "Returns a text slice that starts at the given 0-based index and has given length. Start and length are measured in bytes.", + "slice_back[byte]": + "Returns a text slice that starts at the given -1-based backwards index and has given length. Start and length are measured in bytes.", + "slice[Ascii]": + "Returns a text slice that starts at the given 0-based index and has given length.", + "slice_back[Ascii]": + "Returns a text slice that starts at the given -1-based backwards index and has given length.", + "slice[List]": + "Returns a list slice that starts at the given 0-based index and has given length.", + "slice_back[List]": + "Returns a list slice that starts at the given -1-based backwards index and has given length.", + + // Chars + "ord_at[byte]": + "Gets the byte (as integer) at the 0-based index (counting bytes).", + "ord_at_back[byte]": + "Gets the byte (as integer) at the -1-based backwards index (counting bytes).", + "ord_at[codepoint]": + "Gets the codepoint (as integer) at the 0-based index (counting codepoints).", + "ord_at_back[codepoint]": + "Gets the codepoint (as integer) at the -1-based backwards index (counting codepoints).", + "ord_at[Ascii]": "Gets the character (as integer) at the 0-based index.", + "ord_at_back[Ascii]": + "Gets the character (as integer) at the -1-based backwards index.", + "ord[byte]": "Converts the byte to an integer.", + "ord[codepoint]": "Converts the codepoint to an integer.", + "ord[Ascii]": "Converts the character to an integer.", + "char[byte]": "Returns a byte (as text) corresponding to the integer.", + "char[codepoint]": + "Returns a codepoint (as text) corresponding to the integer.", + "char[Ascii]": "Returns a character corresponding to the integer.", + // Order + "sorted[Int]": "Returns a sorted copy of the input.", + "sorted[Ascii]": "Returns a lexicographically sorted copy of the input.", + "reversed[byte]": "Returns a text in which the bytes are in reversed order.", + "reversed[codepoint]": + "Returns a text in which the codepoints are in reversed order.", + "reversed[Ascii]": + "Returns a text in which the characters are in reversed order.", + "reversed[List]": "Returns a list in which the items are in reversed order.", + "find[codepoint]": + "Returns a 0-based index of the first codepoint at which the search text starts, provided it is included.", + "find[byte]": + "Returns a 0-based index of the first byte at which the search text starts, provided it is included.", + "find[Ascii]": + "Returns a 0-based index of the first character at which the search text starts, provided it is included.", + "find[List]": + "Returns a 0-based index of the first occurence of the searched item, provided it is included.", + + // Membership + "contains[Array]": "Asserts whether an item is included in the array.", + "contains[List]": "Asserts whether an item is included in the list.", + "contains[Table]": + "Asserts whether an item is included in the keys of the table.", + "contains[Set]": "Asserts whether an item is included in the set.", + "contains[Text]": + "Asserts whether the 2nd argument is a substring of the 1st one.", + + // Size + "size[List]": "Returns the length of the list.", + "size[Set]": "Returns the cardinality of the set.", + "size[Table]": "Returns the number of keys in the table.", + "size[Ascii]": "Returns the length of the text.", + "size[codepoint]": "Returns the length of the text in codepoints.", + "size[byte]": "Returns the length of the text in bytes.", + + // Adding items + include: "Modifies the set by including the given item.", + push: "Modifies the list by pushing the given item at the end.", + append: "Returns a new list with the given item appended at the end.", + "concat[List]": "Returns a new list formed by concatenation of the inputs.", + "concat[Text]": "Returns a new text formed by concatenation of the inputs.", + + // Text ops + repeat: "Repeats the text a given amount of times.", + split: "Splits the text by the delimiter.", + split_whitespace: "Splits the text by any whitespace.", + join: "Joins the items using the delimiter.", + right_align: "Right-aligns the text using spaces to a minimum length.", + replace: "Replaces all occurences of a given text with another text.", + text_multireplace: + "Performs simultaneos replacement of multiple pairs of texts.", + starts_with: "Checks whether the second argument is a prefix of the first.", + ends_with: "Checks whether the second argument is a suffix of the first.", + + // Text / Bool <-> Int + int_to_bin_aligned: + "Converts the integer to a 2-base text and alignes to a minimum length.", + int_to_hex_aligned: + "Converts the integer to a 16-base text and alignes to a minimum length.", + int_to_dec: "Converts the integer to a 10-base text.", + int_to_bin: "Converts the integer to a 2-base text.", + int_to_hex: "Converts the integer to a 16-base text.", + int_to_bool: "Converts 0 to false and 1 to true.", + dec_to_int: "Parses a integer from a 10-base text.", + bool_to_int: "Converts false to 0 and true to 1.", +}; + +export type OpCodeFrontName = + | { + [K in AnyOpCode]: (typeof opCodeDefinitions)[K] extends { front: string } + ? (typeof opCodeDefinitions)[K]["front"] + : K; + }[AnyOpCode] + | AnyOpCode; + +export type OpCode = {}> = { + [K in AnyOpCode]: (typeof opCodeDefinitions)[K] extends T ? K : never; +}[AnyOpCode] & + string; +export type NullaryOpCode = OpCode<{ args: Readonly<[]> }>; +export type UnaryOpCode = OpCode<{ args: Readonly<[Type]> }>; +export type BinaryOpCode = OpCode<{ args: Readonly<[Type, Type]> }>; +export type TernaryOpCode = OpCode<{ args: Readonly<[Type, Type, Type]> }>; +export type VariadicOpCode = { + [K in AnyOpCode]: OpCodeArgTypes extends readonly [...Type[], Rest] + ? K + : never; +}[AnyOpCode]; +export type AssociativeOpCode = OpCode<{ assoc: true }>; +export type CommutativeOpCode = OpCode<{ commutes: true }>; +export type ConversionOpCode = UnaryOpCode & `${string}_${string}`; + +export function isNullary(op: OpCode): op is NullaryOpCode { + return arity(op) === 0; +} +export function isUnary(op: OpCode): op is UnaryOpCode { + return arity(op) === 1; +} export function isBinary(op: OpCode): op is BinaryOpCode { - return BinaryOpCodes.includes(op as any); + return arity(op) === 2; +} +export function isTernary(op: OpCode): op is TernaryOpCode { + return arity(op) === 3; +} +export function isVariadic(op: OpCode): op is VariadicOpCode { + return arity(op) === -1; +} +export function isAssociative(op: OpCode): op is AssociativeOpCode { + return (opCodeDefinitions[op] as any)?.assoc === true; +} +export function isCommutative(op: OpCode): op is CommutativeOpCode { + return (opCodeDefinitions[op] as any)?.commutes === true; } -export const OpCodes = [ - ...BinaryOpCodes, - ...UnaryOpCodes, - "read_codepoint", - "read_byte", - "read_int", - "read_line", - "true", - "false", - "argv", - "argc", - "text_replace", - "text_multireplace", // simultaneous replacement. Equivalent to chained text_replace if the inputs and outputs have no overlap - "text_get_codepoint_slice", // Returns a slice of the input text. Indeces are codepoint-0-based, start is inclusive, end is exclusive. - "text_get_byte_slice", // Returns a slice of the input text. Indeces are byte-0-based, start is inclusive, end is exclusive. - // collection set - "array_set", - "list_set", - "table_set", - "println_many_joined", // Expects one text argument denoting the delimiter and then any number of texts to be joined and printed. -] as const; - -export type OpCode = string & (typeof OpCodes)[number]; +export const OpCodes = Object.keys(opCodeDefinitions) as OpCode[]; +export const NullaryOpCodes = OpCodes.filter(isNullary); +export const UnaryOpCodes = OpCodes.filter(isUnary); +export const BinaryOpCodes = OpCodes.filter(isBinary); +export const TernaryOpCodes = OpCodes.filter(isTernary); +export const VariadicOpCodes = OpCodes.filter(isVariadic); +export const AssociativeOpCodes = OpCodes.filter(isAssociative); +export const CommutativeOpCodes = OpCodes.filter(isCommutative); export function isOpCode(op: string): op is OpCode { - return OpCodes.includes(op as any); + return op in opCodeDefinitions; } -/** - * Returns parite of an op, -1 denotes variadic. - */ -export function arity(op: OpCode): number { - if (isUnary(op)) return 1; - if (isBinary(op)) return 2; - switch (op) { - case "true": - case "false": - case "argv": - case "argc": - case "read_byte": - case "read_codepoint": - case "read_int": - case "read_line": - return 0; - case "text_replace": - case "text_get_byte_slice": - case "text_get_codepoint_slice": - case "array_set": - case "list_set": - case "table_set": - return 3; - case "println_many_joined": - case "text_multireplace": - return -1; +export const OpCodeFrontNames = [ + ...new Set([ + ...Object.entries(opCodeDefinitions).map(([k, v]) => + "front" in v && typeof v.front === "string" ? v.front : k, + ), + ...OpCodes, + ]), +]; + +export const OpCodeFrontNamesToOpCodes = Object.fromEntries( + OpCodeFrontNames.map((frontName) => [ + frontName, + OpCodes.filter( + (op) => + op === frontName || (opCodeDefinitions[op] as any).front === frontName, + ), + ]), +) as Record; + +export const OpCodesUser = OpCodes.filter( + (op) => "front" in opCodeDefinitions[op], +); + +export function userName(opCode: OpCode) { + if ("front" in opCodeDefinitions[opCode]) { + return typeof (opCodeDefinitions[opCode] as any).front === "string" + ? (opCodeDefinitions[opCode] as any).front + : opCode; } } /** - * Maps a binary op to another one with the same meaning, except the order of the arguments is swapped. - * This should only be used for ops that are *not* associative. + * Returns parity of an op, -1 denotes variadic. */ -export function flipOpCode(op: BinaryOpCode): BinaryOpCode | null { - switch (op) { - case "eq": - case "neq": - return op; - case "lt": - return "gt"; - case "gt": - return "lt"; - case "leq": - return "geq"; - case "geq": - return "leq"; +export function arity(op: OpCode): number { + try { + const args = opCodeDefinitions[op].args; + if (args.length > 0 && "rest" in args.at(-1)!) return -1; + return args.length; + } catch (e) { + console.log("arity of", op); + throw e; } - return null; } -export function booleanNotOpCode(op: BinaryOpCode): BinaryOpCode | null { - switch (op) { - case "eq": - return "neq"; - case "neq": - return "eq"; - case "lt": - return "geq"; - case "gt": - return "leq"; - case "leq": - return "gt"; - case "geq": - return "lt"; +export function matchesOpCodeArity(op: OpCode, arity: number) { + const expectedTypes = opCodeDefinitions[op].args; + if (expectedTypes.length > 0 && "rest" in expectedTypes.at(-1)!) { + return arity >= expectedTypes.length - 1; } - return null; + return expectedTypes.length === arity; } -export type AliasedOpCode = [Alias] extends [X] - ? [X] extends [Alias | infer Additional extends OpCode] - ? Alias | Additional - : Otherwise - : Otherwise; +/** + * Maps a binary op to another one with the same meaning, except the order of the arguments is swapped. + * This should only be used for ops that are *not* associative or commutative. + */ +export const flippedOpCode = { + lt: "gt", + gt: "lt", + leq: "geq", + geq: "leq", +} as const satisfies Partial>; + +export const booleanNotOpCode = { + "eq[Int]": "neq[Int]", + "eq[Text]": "neq[Text]", + "neq[Int]": "eq[Int]", + "neq[Text]": "eq[Text]", + lt: "geq", + gt: "leq", + leq: "gt", + geq: "lt", +} as const satisfies Partial>; + +export const inverseOpCode = { + bool_to_int: "int_to_bool", + int_to_bool: "bool_to_int", + int_to_dec: "dec_to_int", + not: "not", + bit_not: "bit_not", +} as const satisfies Partial>; + +export const infixableOpCodeNames = [ + "+", + "-", + "*", + "^", + "&", + "|", + "~", + ">>", + "<<", + "==", + "!=", + "<=", + "<", + ">=", + ">", + "#", + "@", + "mod", + "rem", + "div", + "trunc_div", +] as const satisfies readonly OpCodeFrontName[]; diff --git a/src/IR/terminals.ts b/src/IR/terminals.ts index d661e43c..622480fa 100644 --- a/src/IR/terminals.ts +++ b/src/IR/terminals.ts @@ -46,13 +46,15 @@ export interface Text extends BaseNode { } export type IDCastable = string | Identifier; + export function castID(name: IDCastable) { if (typeof name === "string") return id(name); return name; } -export function id(name: string, builtin: boolean = false): Identifier { - return { kind: "Identifier", name, builtin }; +let unique = 0; +export function id(name?: string, builtin: boolean = false): Identifier { + return { kind: "Identifier", name: name ?? `unique#${unique++}`, builtin }; } export function builtin(name: string): Identifier { diff --git a/src/IR/types.ts b/src/IR/types.ts index 03279c34..c97cca54 100644 --- a/src/IR/types.ts +++ b/src/IR/types.ts @@ -1,6 +1,16 @@ import { getType } from "../common/getType"; import { type Spine } from "../common/Spine"; -import { type Node, array, list, set, table, text, int } from "./IR"; +import { + type Node, + array, + list, + set, + table, + text, + int, + op, + keyValue, +} from "./IR"; /** The type of the value of a node when evaluated */ export interface IntegerType { @@ -8,6 +18,11 @@ export interface IntegerType { readonly low: IntegerBound; readonly high: IntegerBound; } +export interface ArrayIndexType { + readonly kind: "integer"; + readonly low: 0n; + readonly high: bigint; +} export type IntegerBound = bigint | "-oo" | "oo"; export interface TextType { @@ -27,13 +42,13 @@ export interface FunctionType { } export interface TableType { kind: "Table"; - key: IntegerType | TextType; + key: IntegerType | TextType | TypeArg; value: Type; } export interface ArrayType { kind: "Array"; member: Type; - length: number; + length: ArrayIndexType | TypeArg; } export interface ListType { kind: "List"; @@ -43,6 +58,11 @@ export interface SetType { kind: "Set"; member: Type; } +export interface TypeArg { + kind: "TypeArg"; + name: string; +} + export type Type = | FunctionType | IntegerType @@ -53,7 +73,8 @@ export type Type = | TableType | KeyValueType | ArrayType - | SetType; + | SetType + | TypeArg; export const booleanType: Type = { kind: "boolean" }; export const voidType: Type = { kind: "void" }; @@ -86,6 +107,15 @@ export function type( } } +export function isArrayIndexType(type: Type): type is ArrayIndexType { + return ( + type.kind === "integer" && + type.low === 0n && + typeof type.high === "bigint" && + type.high >= 0n + ); +} + export function functionType(args: Type[], result: Type): FunctionType { return { kind: "Function", @@ -106,7 +136,7 @@ export function keyValueType( } export function tableType( - key: IntegerType | TextType, + key: IntegerType | TextType | TypeArg, value: Type | "void" | "boolean", ): TableType { return { @@ -132,12 +162,15 @@ export function listType(member: Type | "void" | "boolean"): ListType { export function arrayType( member: Type | "void" | "boolean", - length: number, + length: number | ArrayIndexType | TypeArg, ): ArrayType { return { kind: "Array", member: type(member), - length, + length: + typeof length === "number" + ? { kind: "integer", low: 0n, high: BigInt(length - 1) } + : length, }; } @@ -187,6 +220,10 @@ export function textType( }; } +export function typeArg(name: string): TypeArg { + return { kind: "TypeArg", name }; +} + export const asciiType = textType(integerType(0), true); export function integerTypeIncludingAll( @@ -225,7 +262,11 @@ export function toString(a: Type): string { case "List": return `(List ${toString(a.member)})`; case "Array": - return `(Array ${toString(a.member)} ${a.length})`; + return `(Array ${toString(a.member)} ${ + a.length.kind === "TypeArg" + ? toString(a.length) + : (a.length.high + 1n).toString() + })`; case "Set": return `(Set ${toString(a.member)})`; case "Table": @@ -242,7 +283,11 @@ export function toString(a: Type): string { case "boolean": return "Bool"; case "integer": - return `${a.low.toString()}..${a.high.toString()}`; + return a.low === "-oo" && a.high === "oo" + ? "Int" + : `${a.low.toString()}..${a.high.toString()}`; + case "TypeArg": + return a.name; } } @@ -462,3 +507,98 @@ export function defaultValue(a: Type): Node { } throw new Error(`Unsupported default value for type ${toString(a)}`); } + +export function instantiateGenerics( + typeParams: Record, +): (type: Type) => Type { + function instantiate(type: Type): Type { + switch (type.kind) { + case "Array": { + const lengthType = instantiate(type.length); + if (lengthType.kind !== "TypeArg" && !isArrayIndexType(lengthType)) + throw new Error( + "Array type's second argument must be a constant integer type.", + ); + return arrayType(instantiate(type.member), lengthType); + } + case "Function": + return functionType( + type.arguments.map(instantiate), + instantiate(type.result), + ); + case "KeyValue": { + const keyType = instantiate(type.key); + if (keyType.kind !== "integer" && keyType.kind !== "text") + throw new Error( + "KeyValue type's first argument must be an integer or text type.", + ); + return keyValueType(keyType, instantiate(type.value)); + } + case "Table": { + const keyType = instantiate(type.key); + if ( + keyType.kind !== "integer" && + keyType.kind !== "text" && + keyType.kind !== "TypeArg" + ) + throw new Error( + "Table type's first argument must be an integer or text type.", + ); + return tableType(keyType, instantiate(type.value)); + } + case "List": + return listType(instantiate(type.member)); + case "Set": + return setType(instantiate(type.member)); + case "boolean": + case "integer": + case "text": + case "void": + return type; + case "TypeArg": + return typeParams[type.name] ?? type; + } + } + return instantiate; +} + +export function getLiteralOfType(type: Type, nonEmpty = false): Node { + switch (type.kind) { + case "text": + return text(nonEmpty ? "x" : ""); + case "integer": + return int( + leq(type.low, 1n) && leq(1n, type.high) + ? 1n + : type.low === "-oo" + ? (type.high as bigint) + : (type.low as bigint), + ); + case "boolean": + return op.true; + case "Array": + if (isArrayIndexType(type.length)) + return array( + Array(Number(type.length.high) - 1).fill( + getLiteralOfType(type.member, nonEmpty), + ), + ); + break; + case "List": + return list(nonEmpty ? [getLiteralOfType(type.member, nonEmpty)] : []); + case "Set": + return set(nonEmpty ? [getLiteralOfType(type.member, nonEmpty)] : []); + case "Table": + return table( + nonEmpty + ? [ + keyValue( + getLiteralOfType(type.key, nonEmpty), + getLiteralOfType(type.value, nonEmpty), + ), + ] + : [], + ); + } + throw new Error(`There's no literal of type '${type.kind}'.`); +} diff --git a/src/cli.ts b/src/cli.ts index 1c21028a..133d11f6 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -3,9 +3,10 @@ import yargs from "yargs"; import fs from "fs"; import path from "path"; -import compile from "./common/compile"; +import compile, { debugEmit } from "./common/compile"; import { PolygolfError } from "./common/errors"; import languages, { findLang } from "./languages/languages"; +import { EmitError } from "./common/emit"; const languageChoices = [ ...new Set(languages.flatMap((x) => [x.name.toLowerCase(), x.extension])), @@ -62,12 +63,8 @@ const printingMultipleLangs = langs.length > 1 && options.output === undefined; for (const result of compile( code, { - level: "full", objective: options.chars === true ? "chars" : "bytes", getAllVariants: options.all === true, - codepointRange: [1, Infinity], - restrictFrontend: true, - skipTypecheck: false, }, ...langs, )) { @@ -92,6 +89,19 @@ for (const result of compile( if (options.debug === true) { console.log("History:"); console.log(result.history.map(([c, name]) => `${c} ${name}`).join("\n")); + + if (result.errors.length > 0) { + console.log("Errors:"); + console.log( + result.errors + .map( + (e) => + e.message + + (e instanceof EmitError ? "\n" + debugEmit(e.expr) : ""), + ) + .join("\n"), + ); + } } } else { if (!printingMultipleLangs && langs.length > 1) diff --git a/src/common/Language.ts b/src/common/Language.ts index 276ba054..05898e76 100644 --- a/src/common/Language.ts +++ b/src/common/Language.ts @@ -27,6 +27,7 @@ export interface Language { extension: string; phases: LanguagePhase[]; emitter: Emitter; + noEmitter?: Emitter; // emitter used with the `noEmit` flag packers?: Packer[]; detokenizer?: Detokenizer; readsFromStdinOnCodeDotGolf?: boolean; @@ -39,33 +40,42 @@ export interface LanguagePhase { plugins: Plugin[]; } -export function required(...plugins: Plugin[]): LanguagePhase { +function languagePhase( + mode: LanguagePhaseMode, + plugins: (Plugin | PluginVisitor)[], +): LanguagePhase { return { - mode: "required", - plugins, + mode, + plugins: plugins.map((x) => + typeof x === "function" ? { name: x.name, visit: x } : x, + ), }; } -export function simplegolf(...plugins: Plugin[]): LanguagePhase { - return { - mode: "simplegolf", - plugins, - }; +export function required( + ...plugins: (Plugin | PluginVisitor)[] +): LanguagePhase { + return languagePhase("required", plugins); } -export function search(...plugins: Plugin[]): LanguagePhase { - return { - mode: "search", - plugins, - }; +export function simplegolf( + ...plugins: (Plugin | PluginVisitor)[] +): LanguagePhase { + return languagePhase("simplegolf", plugins); +} + +export function search(...plugins: (Plugin | PluginVisitor)[]): LanguagePhase { + return languagePhase("search", plugins); } export interface Plugin { name: string; + /** If set, annotates the replacement with the calculated type of the original node. */ + bakeType?: boolean; /** visit should return one or more viable replacement nodes, or undefined to represent * no replacement. The replacement nodes should be different in value than * the initial node if it compares different under reference equality */ - visit: PluginVisitor; + visit: PluginVisitor; } type TokenTreeArray = Array; @@ -88,6 +98,7 @@ export interface IdentifierGenerator { preferred: (original: string) => string[]; short: string[]; general: (i: number) => string; + reserved: string[]; } export type Emitter = ( diff --git a/src/common/Spine.ts b/src/common/Spine.ts index 3629b5c8..4565996b 100644 --- a/src/common/Spine.ts +++ b/src/common/Spine.ts @@ -1,5 +1,5 @@ -import { type IR, isOp, op, block } from "../IR"; -import { type CompilationContext } from "./compile"; +import { type IR, isOp, op, block, isOfKind, type Node } from "../IR"; +import type { VisitorContext, CompilationContext } from "./compile"; import { getChild, getChildFragments, @@ -79,26 +79,29 @@ export class Spine { if (this.parent === null || this.pathFragment === null) { return new Spine(newNode, null, null); } - if (newNode.kind === "Block" && this.parent.node.kind === "Block") { - throw new Error( - `Programming error: attempt to insert a Block into a Block`, - ); - } const parentNode = this.parent.node; const parent = canonizeAndReturnRoot && - isOp()(parentNode) && + isOfKind("Op", "Block")(parentNode) && typeof this.pathFragment === "object" ? this.parent.replacedWith( { - ...op( - parentNode.op, - ...replaceAtIndex( - parentNode.args, - this.pathFragment.index, - newNode, - ), - ), + ...(isOp()(parentNode) + ? op.unsafe( + parentNode.op, + ...replaceAtIndex( + parentNode.args, + this.pathFragment.index, + newNode, + ), + ) + : block( + replaceAtIndex( + parentNode.children, + this.pathFragment.index, + newNode, + ), + )), targetType: parentNode.targetType, }, true, @@ -113,9 +116,16 @@ export class Spine { * by removal of `undefined` return values. Returns a generator, so is a no-op * if the values are not used. Name inspired by Swift's `compactMap`. */ *compactMap(func: Visitor): Generator { - const ret = func(this.node, this); + let skipChildren = false; + const ret = func(this.node, this, { + skipChildren() { + skipChildren ||= true; + }, + skipReplacement() {}, + }); if (ret !== undefined) yield ret; - for (const child of this.getChildSpines()) yield* child.compactMap(func); + if (!skipChildren) + for (const child of this.getChildSpines()) yield* child.compactMap(func); } /** Test whether this node and all children meet the provided condition. */ @@ -130,9 +140,14 @@ export class Spine { return false; } - /** Returns all descendants metting the provided condition. */ + /** Returns all descendants meeting the provided condition. */ filterNodes(cond: Visitor) { - return this.compactMap((n, s) => (cond(n, s) ? n : undefined)); + return this.compactMap((n, s, skip) => (cond(n, s, skip) ? n : undefined)); + } + + /** Counts the descendants meeting the provided condition. */ + countNodes(cond: Visitor) { + return [...this.filterNodes(cond)].length; } /** Return the spine (pointing to this node) determined from replacing this @@ -148,13 +163,25 @@ export class Spine { skipReplaced = false, skipThis = false, ): Spine { - const ret = skipThis ? undefined : replacer(this.node, this); - if (ret === undefined) { + let skipReplacement = skipReplaced; + let skipChildren = false; + const ret = skipThis + ? undefined + : replacer(this.node, this, { + skipChildren() { + skipChildren ||= true; + }, + skipReplacement() { + skipReplacement ||= true; + }, + }); + if (ret === undefined && skipChildren) return this; + else if (ret === undefined) { // eslint-disable-next-line @typescript-eslint/no-this-alias let curr = this as Spine; // recurse on children - if (isOp()(this.node)) { - // Create canonical Op instead of just replacing the chidren + if (isOfKind("Op", "Block")(this.node)) { + // Create canonical Op / block instead of just replacing the chidren const newChildren: IR.Node[] = []; let someChildrenIsNew = false; for (const child of this.getChildSpines()) { @@ -164,7 +191,9 @@ export class Spine { } if (someChildrenIsNew) curr = curr.replacedWith({ - ...op(this.node.op, ...newChildren), + ...(isOp()(this.node) + ? op.unsafe(this.node.op, ...newChildren) + : block(newChildren)), targetType: this.node.targetType, }); } else { @@ -178,47 +207,26 @@ export class Spine { } } return curr; - } else if (skipReplaced) { + } else if (skipReplacement || skipChildren) { return this.replacedWith(ret); } else { // replace this, then recurse on children but not this return this.replacedWith(ret).withReplacer(replacer, skipReplaced, true); } } - - flatMapWithChildrenReplacer( - replacer: Visitor, - ): IR.Node | undefined { - if (this.isRoot) { - const repl = replacer(this.node, this); - if (repl !== undefined) return block(repl); - } - if (this.node.kind !== "Block") return; - const children = this.node.children; - let newChildren: IR.Node[] | undefined; - for (let i = 0; i < children.length; i++) { - const child = this.getChild({ prop: "children", index: i }); - const replacement = replacer(child.node, child); - if (replacement !== undefined) { - if (newChildren === undefined) { - newChildren = children.slice(0, i); - } - newChildren.push(...replacement); - } else if (newChildren !== undefined) { - newChildren.push(child.node); - } - } - if (newChildren !== undefined) return block(newChildren); - } } -export type PluginVisitor = ( - node: N, - spine: Spine, +export type PluginVisitor = ( + node: Node, + spine: Spine, context: CompilationContext, ) => T; -export type Visitor = (node: N, spine: Spine) => T; +export type Visitor = ( + node: Node, + spine: Spine, + context: VisitorContext, +) => T; export function programToSpine(node: IR.Node) { return new Spine(node, null, null); diff --git a/src/common/arrays.ts b/src/common/arrays.ts index 4ab41db5..2a508cde 100644 --- a/src/common/arrays.ts +++ b/src/common/arrays.ts @@ -29,11 +29,19 @@ export function filterInplace(data: T[], predicate: (x: T) => boolean) { data.length = length; } +export function mapObjectValues( + obj: Record, + f: (v: T2, k: T1) => T3, +): Record; +export function mapObjectValues( + obj: Partial>, + f: (v: T2, k: T1) => T3, +): Partial>; export function mapObjectValues( obj: Partial>, f: (v: T2, k: T1) => T3, ) { return Object.fromEntries( Object.entries(obj).map(([k, v]) => [k as T1, f(v as T2, k as T1)]), - ) as Partial>; + ); } diff --git a/src/common/compile.test.ts b/src/common/compile.test.ts index a2dfc479..6f8569b1 100644 --- a/src/common/compile.test.ts +++ b/src/common/compile.test.ts @@ -17,7 +17,7 @@ const textLang: Language = { emitter(program, context) { return (program.kind === "Block" ? program.children : [program]).map( (x) => { - if (isOp()(x) && isText()(x.args[0])) { + if (isOp()(x) && x.args.length > 0 && isText()(x.args[0]!)) { if (x.args[0].value.endsWith("X")) { context.addWarning(new PolygolfError("global warning"), true); context.addWarning( diff --git a/src/common/compile.ts b/src/common/compile.ts index c284a348..da7c1c5f 100644 --- a/src/common/compile.ts +++ b/src/common/compile.ts @@ -1,21 +1,27 @@ -import { type Node } from "../IR"; +import { isOp, op, isOpCode, type Type, type Node } from "../IR"; import { expandVariants } from "./expandVariants"; -import { defaultDetokenizer, type Plugin, type Language } from "./Language"; +import { + defaultDetokenizer, + type Plugin, + type Language, + type TokenTree, +} from "./Language"; import { programToSpine, type Spine } from "./Spine"; -import { getType } from "./getType"; +import { getType, getTypeAndResolveOpCode } from "./getType"; import { stringify } from "./stringify"; -import parse from "../frontend/parse"; +import parse, { type ParseResult } from "../frontend/parse"; import { MinPriorityQueue } from "@datastructures-js/priority-queue"; import polygolfLanguage from "../languages/polygolf"; import { type Objective, type ObjectiveFunc, - charLength, getObjectiveFunc, shorterBy, } from "./objective"; import { readsFromArgv, readsFromStdin } from "./symbols"; import { PolygolfError } from "./errors"; +import { charLength } from "./strings"; +import { getOutput } from "../interpreter"; export type OptimisationLevel = "nogolf" | "simple" | "full"; export interface CompilationOptions { @@ -25,11 +31,33 @@ export interface CompilationOptions { skipTypecheck: boolean; restrictFrontend: boolean; codepointRange: [number, number]; + skipPlugins: string[]; + noEmit: boolean; +} + +export function defaultCompilationOptions( + partial: Partial = {}, +): CompilationOptions { + return { + level: partial.level ?? "full", + objective: partial.objective ?? "bytes", + getAllVariants: partial.getAllVariants ?? false, + skipTypecheck: partial.skipTypecheck ?? false, + restrictFrontend: partial.restrictFrontend ?? true, + codepointRange: partial.codepointRange ?? [1, Infinity], + skipPlugins: partial.skipPlugins ?? [], + noEmit: partial.noEmit ?? false, + }; } export type AddWarning = (x: Error, isGlobal: boolean) => void; -export interface CompilationContext { +export interface VisitorContext { + skipReplacement: () => void; // Prevents recursion into a new node. + skipChildren: () => void; // Prevents any recursion = into a new node or into children of old node. +} + +export interface CompilationContext extends VisitorContext { options: CompilationOptions; addWarning: AddWarning; } @@ -37,6 +65,7 @@ export interface CompilationContext { export interface CompilationResult { language: string; result: string | Error; + errors: Error[]; history: [number, string][]; warnings: Error[]; } @@ -44,12 +73,14 @@ export interface CompilationResult { function compilationResult( language: string, result: string | Error, + errors: Error[], history: [number, string][] = [], warnings: Error[] = [], ): CompilationResult { return { language, result, + errors, history, warnings, }; @@ -57,14 +88,15 @@ function compilationResult( export function applyAllToAllAndGetCounts( program: Node, - context: CompilationContext, - ...visitors: Plugin["visit"][] + options: CompilationOptions, + addWarning: AddWarning, + ...plugins: Plugin[] ): [Node, number[]] { const counts: number[] = []; let result = program; let c: number; - for (const visitor of visitors) { - [result, c] = applyToAllAndGetCount(result, context, visitor); + for (const plugin of plugins) { + [result, c] = applyToAllAndGetCount(result, options, addWarning, plugin); counts.push(c); } return [result, counts]; @@ -92,37 +124,59 @@ function getArray(x: T | T[] | undefined): T[] { export function applyToAllAndGetCount( program: Node, - context: CompilationContext, - visitor: Plugin["visit"], + options: CompilationOptions, + addWarning: AddWarning, + plugin: Plugin, ): [Node, number] { - const result = programToSpine(program).withReplacer((n, s) => { - const repl = getSingleOrUndefined(visitor(n, s, context)); - return repl === undefined - ? undefined - : copySource(n, copyTypeAnnotation(n, repl)); + const result = programToSpine(program).withReplacer((n, s, ctx) => { + const repl = getSingleOrUndefined( + plugin.visit(n, s, { options, addWarning, ...ctx }), + ); + return annotate(repl, s, plugin.bakeType === true); }).node; return [result, program === result ? 0 : 1]; // TODO it might be a bit more informative to count the actual replacements, intead of returning 1 } function* applyToOne( spine: Spine, - context: CompilationContext, - visitor: Plugin["visit"], + options: CompilationOptions, + addWarning: AddWarning, + plugin: Plugin, ) { - for (const altPrograms of spine.compactMap((n, s) => { - const suggestions = getArray(visitor(n, s, context)); + for (const altPrograms of spine.compactMap((n, s, ctx) => { + const suggestions = getArray( + plugin.visit(n, s, { options, addWarning, ...ctx }), + ); return suggestions.map( (x) => - s.replacedWith(copySource(n, copyTypeAnnotation(n, x)), true).root.node, + s.replacedWith(annotate(x, s, plugin.bakeType === true), true).root + .node, ); })) { yield* altPrograms; } } -function emit(language: Language, program: Node, context: CompilationContext) { - return (language.detokenizer ?? defaultDetokenizer())( - language.emitter(program, context), - ); +function emit( + language: Language, + program: Node, + context: CompilationContext, + noEmit: boolean, +) { + let tokenTree: TokenTree; + if (noEmit) { + if (language.noEmitter !== undefined) { + try { + tokenTree = language.noEmitter(program, context); + } catch { + tokenTree = debugEmit(program); + } + } else { + tokenTree = debugEmit(program); + } + } else { + tokenTree = language.emitter(program, context); + } + return (language.detokenizer ?? defaultDetokenizer())(tokenTree); } function isError(x: any): x is Error { @@ -131,23 +185,24 @@ function isError(x: any): x is Error { export default function compile( source: string, - options: CompilationOptions, + partialOptions: Partial, ...languages: Language[] ): CompilationResult[] { + const options = defaultCompilationOptions(partialOptions); const obj = getObjectiveFunc(options); - let program: Node; + let parsed: ParseResult; try { - program = parse(source, options.restrictFrontend); + parsed = parse(source, options.restrictFrontend); } catch (e) { - if (isError(e)) return [compilationResult("Polygolf", e)]; + if (isError(e)) return [compilationResult("Polygolf", e, [e])]; } - program = program!; + const program = parsed!.node; let variants = expandVariants(program).map((x) => { try { - if (!options.skipTypecheck) typecheck(x); + x = typecheck(x, !options.skipTypecheck); return x; } catch (e) { - if (isError(e)) return compilationResult("Polygolf", e); + if (isError(e)) return compilationResult("Polygolf", e, [e]); throw e; } }); @@ -157,6 +212,9 @@ export default function compile( (x) => "result" in x, ) as CompilationResult[]; if (errorlessVariants.length === 0) { + for (const variant of variants) { + (variant as CompilationResult).warnings = parsed!.warnings; + } if (options.getAllVariants) { return variants as CompilationResult[]; } else { @@ -195,6 +253,10 @@ export default function compile( result.push(errorVariants[0]); } + for (const res of result) { + res.warnings.push(...parsed!.warnings); + } + return result; } @@ -226,9 +288,14 @@ function getVariantsByInputMethod(variants: Node[]): Map { export function compileVariant( program: Node, - options: CompilationOptions, + partialOptions: Partial, language: Language, ): CompilationResult { + const options = defaultCompilationOptions(partialOptions); + if (options.level !== "nogolf") + try { + getOutput(program); // precompute output + } catch {} const obj = getObjectiveFunc(options); let best = compileVariantNoPacking(program, options, language); const packers = language.packers ?? []; @@ -276,11 +343,19 @@ interface SearchState { export function compileVariantNoPacking( program: Node, - options: CompilationOptions, + partialOptions: Partial, language: Language, ): CompilationResult { - const phases = language.phases; - if (options.level === "nogolf" || options.level === "simple") { + const options = defaultCompilationOptions(partialOptions); + const phases = language.phases.map((x) => ({ + mode: x.mode, + plugins: x.plugins.filter((x) => !options.skipPlugins.includes(x.name)), + })); + if ( + phases.length < 1 || + options.level === "nogolf" || + options.level === "simple" + ) { try { const warnings: Error[] = []; const addWarning = (x: Error) => warnings.push(x); @@ -293,18 +368,30 @@ export function compileVariantNoPacking( .flatMap((x) => x.plugins); const [res, counts] = applyAllToAllAndGetCounts( program, - { addWarning, options }, - ...plugins.map((x) => x.visit), + options, + addWarning, + ...plugins, ); return compilationResult( language.name, - emit(language, res, { addWarning, options }), + emit( + language, + res, + { + addWarning, + options, + skipChildren() {}, + skipReplacement() {}, + }, + options.noEmit, + ), + [], plugins.map((y, i) => [counts[i], y.name]), warnings, ); } catch (e) { if (isError(e)) { - return compilationResult(language.name, e); + return compilationResult(language.name, e, [e]); } throw e; } @@ -313,7 +400,8 @@ export function compileVariantNoPacking( function finish( prog: Node, addWarning: AddWarning, - startPhase = 0, + startPhase: number, + noEmit: boolean, ): [string, [number, string][]] { const finishingPlugins = phases .slice(startPhase) @@ -321,16 +409,27 @@ export function compileVariantNoPacking( .flatMap((x) => x.plugins); const [resProg, counts] = applyAllToAllAndGetCounts( prog, - { addWarning, options }, - ...finishingPlugins.map((x) => x.visit), + options, + addWarning, + ...finishingPlugins, ); return [ - emit(language, resProg, { addWarning, options }), + emit( + language, + resProg, + { + addWarning, + options, + skipChildren() {}, + skipReplacement() {}, + }, + noEmit, + ), finishingPlugins.map((x, i) => [counts[i], x.name]), ]; } let shortestSoFar: SearchState | undefined; - let lastError: Error; + const errors: Error[] = []; let shortestSoFarLength: number = Infinity; const latestPhaseWeSawTheProg = new Map(); const queue = new MinPriorityQueue((x) => x.length); @@ -342,7 +441,7 @@ export function compileVariantNoPacking( history: [number, string][], warnings: Error[], ) { - if (startPhase >= language.phases.length) return; + if (startPhase >= phases.length) return; if (latestPhaseWeSawTheProg.size > 200) return; const stringified = stringify(program); const latestSeen = latestPhaseWeSawTheProg.get(stringified); @@ -354,7 +453,7 @@ export function compileVariantNoPacking( } try { - const length = obj(finish(program, addWarning, startPhase)[0]); + const length = obj(finish(program, addWarning, startPhase, false)[0]); const state = { program, startPhase, length, history, warnings }; if (shortestSoFar === undefined || length < shortestSoFarLength) { shortestSoFarLength = length; @@ -363,7 +462,7 @@ export function compileVariantNoPacking( queue.enqueue(state); } catch (e) { if (isError(e)) { - lastError = e; + errors.push(e); } } } @@ -373,7 +472,7 @@ export function compileVariantNoPacking( while (!queue.isEmpty()) { const state = queue.dequeue(); - const phase = language.phases[state.startPhase]; + const phase = phases[state.startPhase]; const warnings = [...state.warnings]; function addWarning(x: Error, isGlobal: boolean) { @@ -383,8 +482,9 @@ export function compileVariantNoPacking( if (phase.mode !== "search") { const [res, counts] = applyAllToAllAndGetCounts( state.program, - { addWarning, options }, - ...phase.plugins.map((x) => x.visit), + options, + addWarning, + ...phase.plugins, ); enqueue( res, @@ -403,8 +503,9 @@ export function compileVariantNoPacking( for (const plugin of phase.plugins) { for (const altProgram of applyToOne( spine, - { addWarning, options }, - plugin.visit, + options, + addWarning, + plugin, )) { enqueue( altProgram, @@ -418,7 +519,7 @@ export function compileVariantNoPacking( } if (shortestSoFar === undefined) { - return compilationResult(language.name, lastError!); + return compilationResult(language.name, errors.at(-1)!, errors); } globalWarnings.push(...shortestSoFar.warnings); @@ -429,11 +530,13 @@ export function compileVariantNoPacking( globalWarnings.push(x); }, shortestSoFar.startPhase, + options.noEmit, ); return compilationResult( language.name, result, + errors, mergeRepeatedPlugins([...shortestSoFar.history, ...finishingHist]), globalWarnings, ); @@ -451,24 +554,38 @@ function mergeRepeatedPlugins(history: [number, string][]): [number, string][] { return result; } -function copyTypeAnnotation(from: Node, to: Node): Node { - // copy type annotation if present - return from.type !== undefined ? { ...to, type: from.type } : to; -} - -function copySource(from: Node, to: Node): Node { - // copy source reference if present - return { ...to, source: from.source }; +function annotate( + node: T, + sourceSpine: Spine, + bakeType: boolean, +): T { + if (node === undefined) return node; + let type: Type | undefined; + try { + type = bakeType + ? getType(sourceSpine.node, sourceSpine) + : sourceSpine.node.type ?? node.type; + } catch {} + return { + ...node, + source: sourceSpine.node.source, + type, + targetType: node.targetType ?? sourceSpine.node.targetType, + }; } -/** Typecheck a program by asking all nodes about their types. - * Throws an error on a type error; otherwise is a no-op. */ -function typecheck(program: Node) { +/** Typecheck a program and return a program with resolved opcodes. + * If everyNode is false, typechecks only nodes neccesary to resolve opcodes, otherwise, typechecks every node. */ +export function typecheck(program: Node, everyNode = true): Node { const spine = programToSpine(program); - spine.everyNode((x) => { - getType(x, program); - return true; - }); + return spine.withReplacer(function (node, spine) { + if (everyNode || (node.kind === "Op" && !isOpCode(node.op))) { + const t = getTypeAndResolveOpCode(node, spine); + if (isOp()(node) && t.opCode !== undefined) { + return op.unsafe(t.opCode, ...node.args); + } + } + }).node; } export function debugEmit(program: Node): string { @@ -476,10 +593,7 @@ export function debugEmit(program: Node): string { program, { level: "nogolf", - objective: "bytes", skipTypecheck: true, - getAllVariants: false, - codepointRange: [1, Infinity], restrictFrontend: false, }, polygolfLanguage, @@ -491,5 +605,18 @@ export function debugEmit(program: Node): string { } export function normalize(source: string): string { - return debugEmit(parse(source, false)); + return debugEmit(parse(source, false).node); +} + +export function isCompilable(program: Node, lang: Language) { + const result = compileVariant( + program, + { + level: "nogolf", + restrictFrontend: false, + skipTypecheck: true, + }, + lang, + ); + return typeof result.result === "string"; } diff --git a/src/common/emit.ts b/src/common/emit.ts index 94d05ba7..90f3f048 100644 --- a/src/common/emit.ts +++ b/src/common/emit.ts @@ -1,7 +1,7 @@ import { type IR, type Integer, type Node } from "IR"; import { PolygolfError } from "./errors"; import { type TokenTree } from "./Language"; -import { codepoints } from "./objective"; +import { codepoints } from "./strings"; export function joinTrees( sep: TokenTree, @@ -58,12 +58,14 @@ export function containsMultiNode(exprs: readonly IR.Node[]): boolean { } export class EmitError extends PolygolfError { + expr: Node; constructor(expr: Node, detail?: string) { const kind = expr.kind + ("op" in expr ? `[${expr.op}]` : ""); detail = detail === undefined ? "" : ` (${detail})`; const message = `emit error - ${kind}${detail} not supported.`; super(message, expr.source); this.name = "EmitError"; + this.expr = expr; Object.setPrototypeOf(this, EmitError.prototype); } } diff --git a/src/common/fragments.ts b/src/common/fragments.ts index a94089e8..bdfb5c05 100644 --- a/src/common/fragments.ts +++ b/src/common/fragments.ts @@ -51,7 +51,10 @@ export function* getChildFragments(node: IR.Node): Generator { for (const key of getChildKeys(node)) { const value = (node as any)[key] as IR.Node[] | IR.Node; if (Array.isArray(value)) { - for (const v of value.map((_, i) => ({ prop: key, index: i }))) yield v; + for (const v of value + .filter((x) => typeof x === "object") + .map((_, i) => ({ prop: key, index: i }))) + yield v; } else { yield key; } diff --git a/src/common/getType.test.ts b/src/common/getType.test.ts index 17eb9540..db6a56f6 100644 --- a/src/common/getType.test.ts +++ b/src/common/getType.test.ts @@ -17,7 +17,6 @@ import { variants, toString, voidType, - indexCall, text as textNode, int as intNode, array as arrayNode, @@ -31,9 +30,10 @@ import { forRangeCommon, forDifferenceRange, type Node, + asciiType, } from "IR"; import { PolygolfError } from "./errors"; -import { calcType } from "./getType"; +import { calcTypeAndResolveOpCode, getType } from "./getType"; const ascii = (x: number | IntegerType = int(0)) => text(x, true); @@ -49,8 +49,9 @@ function testNode( prog: Node = block([]), ) { test(name, () => { - if (result === "error") expect(() => calcType(expr, prog)).toThrow(); - else expect(toString(calcType(expr, prog))).toEqual(toString(result)); + if (result === "error") + expect(() => calcTypeAndResolveOpCode(expr, prog)).toThrow(); + else expect(toString(getType(expr, prog))).toEqual(toString(result)); }); } @@ -153,8 +154,10 @@ describe("Assignment", () => { ); test("Self-referential assignment", () => { const aLHS = id("a"); - const expr = assignment(aLHS, op("add", id("a"), e(int(1)))); - expect(() => calcType(aLHS, block([expr]))).toThrow(PolygolfError); + const expr = assignment(aLHS, op.add(id("a"), e(int(1)))); + expect(() => calcTypeAndResolveOpCode(aLHS, block([expr]))).toThrow( + PolygolfError, + ); }); }); @@ -167,39 +170,12 @@ describe("Functions", () => { ); }); -describe("Index call", () => { - testNode("Index int", indexCall(e(int()), e(int())), "error"); - testNode("Index array", indexCall(e(array(int(), 10)), e(int())), "error"); - testNode( - "Index array", - indexCall(e(array(int(), 10)), e(int(10, 10))), - "error", - ); - testNode( - "Index array", - indexCall(e(array(int(), 10)), e(int(0, 0)), true), - "error", - ); - testNode( - "Index array", - indexCall(e(array(text(), 10)), e(int(0, 9))), - text(), - ); - testNode( - "Index list", - indexCall(e(list(int())), e(int(0, 0)), true), - "error", - ); - testNode("Index list", indexCall(e(list(int())), e(int())), "error"); - testNode("Index list", indexCall(e(list(text())), e(int(0))), text()); -}); - describe("Literals", () => { testNode("int", intNode(4n), int(4, 4)); testNode("text", textNode("ahoj"), ascii(int(4, 4))); testNode("text", textNode("dobrý den"), text(int(9, 9))); - testNode("bool", op("true"), bool); - testNode("bool", op("false"), bool); + testNode("bool", op.true, bool); + testNode("bool", op.false, bool); testNode("array", arrayNode([e(int()), e(text())]), "error"); testNode( "array", @@ -352,28 +328,28 @@ describeArithmeticOp("bit_shift_right", [ [[int(10, 50), int(2, 3)], int(1, 12)], ]); -describeOp("print", [ +describeOp("print[Text]", [ [[int()], "error"], [[bool], "error"], [[text(), text()], "error"], [[text()], voidType], ]); -describeOp("println", [ +describeOp("println[Text]", [ [[int()], "error"], [[bool], "error"], [[text(), text()], "error"], [[text()], voidType], ]); -describeOp("print_int", [ +describeOp("print[Int]", [ [[text()], "error"], [[bool], "error"], [[int(), int()], "error"], [[int()], voidType], ]); -describeOp("println_int", [ +describeOp("println[Int]", [ [[text()], "error"], [[bool], "error"], [[int(), int()], "error"], @@ -392,33 +368,33 @@ describeOp("and", [ [[bool, bool], bool], ]); -describeOp("array_contains", [ +describeOp("contains[Array]", [ [[int(), array(int(), 10)], "error"], [[list(int()), int()], "error"], [[array(int(), 10), text()], "error"], [[array(int(), 10), int()], bool], ]); -describeOp("list_contains", [ +describeOp("contains[List]", [ [[int(), list(int())], "error"], [[array(int(), 10), int()], "error"], [[list(int()), text()], "error"], [[list(int()), int()], bool], ]); -describeOp("table_contains_key", [ +describeOp("contains[Table]", [ [[text(), table(text(), int())], "error"], [[table(text(), int()), int()], "error"], [[table(text(), int()), text()], bool], ]); -describeOp("set_contains", [ +describeOp("contains[Set]", [ [[int(), set(int())], "error"], [[set(int()), text()], "error"], [[set(int()), int()], bool], ]); -describeOp("array_get", [ +describeOp("at[Array]", [ [[int(0, 3), array(int(), 4)], "error"], [[array(int(), 4), text()], "error"], [[array(int(), 4), int()], "error"], @@ -426,32 +402,32 @@ describeOp("array_get", [ [[array(int(-300, 300), 4), int(0, 3)], int(-300, 300)], ]); -describeOp("list_get", [ +describeOp("at[List]", [ [[int(0), list(int())], "error"], [[list(int()), text()], "error"], [[list(int()), int()], "error"], [[list(int(-300, 300)), int(0)], int(-300, 300)], ]); -describeOp("table_get", [ +describeOp("at[Table]", [ [[text(), table(text(), int())], "error"], [[table(text(), int()), int()], "error"], [[table(text(5), int()), text()], "error"], [[table(text(), int()), text()], int()], ]); -describeOp("argv_get", [ +describeOp("at[argv]", [ [[int()], "error"], [[int(0)], text()], ]); -describeOp("list_push", [ +describeOp("push", [ [[int(), list(int())], "error"], [[list(int(0, 1000)), int()], "error"], - [[list(int(0, 1000)), int(100, 200)], int(0, 1000)], + [[list(int(0, 1000)), int(100, 200)], voidType], ]); -describeOp("concat", [ +describeOp("concat[Text]", [ [[text(), int()], "error"], [[text(), text()], text()], [[ascii(), text(100, true)], ascii()], @@ -471,19 +447,19 @@ describeOp("repeat", [ [[text(int(10, 20), true), int(3, 5)], text(int(30, 100), true)], ]); -describeOp("text_contains", [ +describeOp("contains[Text]", [ [[text(), int()], "error"], [[text(), text()], bool], ]); -describeOp("text_codepoint_find", [ +describeOp("find[codepoint]", [ [[text(), int()], "error"], [[text(), text()], "error"], [[text(), text(int(1, 1))], int(-1)], [[text(100), text(int(10))], int(-1, 90)], ]); -describeOp("text_byte_find", [ +describeOp("find[byte]", [ [[text(), int()], "error"], [[text(), text()], "error"], [[text(), text(int(1, 1))], int(-1)], @@ -491,47 +467,47 @@ describeOp("text_byte_find", [ [[ascii(100), text(int(10))], int(-1, 90)], ]); -describeOp("text_split", [ +describeOp("split", [ [[text(), int()], "error"], [[text(), text()], listType(text())], [[text(500), text()], listType(text(500))], ]); -describeOp("text_get_byte", [ +describeOp("at[byte]", [ [[text(), text()], "error"], [[text(), int()], "error"], [[text(), int(0)], text(int(1, 1))], [[ascii(), int(0)], ascii(int(1, 1))], ]); -describeOp("text_get_codepoint", [ +describeOp("at[codepoint]", [ [[text(), text()], "error"], [[text(), int()], "error"], [[text(), int(0)], text(int(1, 1))], [[ascii(), int(0)], ascii(int(1, 1))], ]); -describeOp("text_get_codepoint_to_int", [ +describeOp("ord_at[codepoint]", [ [[text(), text()], "error"], [[text(), int()], "error"], [[text(), int(0)], int(0, 0x10ffff)], [[ascii(), int(0)], int(0, 127)], ]); -describeOp("codepoint_to_int", [ +describeOp("ord[codepoint]", [ [[text(), text()], "error"], [[text()], "error"], [[text(int(1, 1))], int(0, 0x10ffff)], ]); -describeOp("text_get_byte_to_int", [ +describeOp("ord_at[byte]", [ [[text(), text()], "error"], [[text(), int()], "error"], [[text(), int(0)], int(0, 255)], [[ascii(), int(0)], int(0, 127)], ]); -describeOp("text_byte_to_int", [ +describeOp("ord[byte]", [ [[text(), text()], "error"], [[text()], "error"], [[ascii(int(1, 1))], int(0, 127)], @@ -586,7 +562,7 @@ describeOp("not", [ [[bool], bool], ]); -describeOp("int_to_text", [ +describeOp("int_to_dec", [ [[bool], "error"], [[text()], "error"], [[int()], ascii(int(1))], @@ -613,7 +589,7 @@ describeOp("int_to_hex", [ [[int(0, 0x10000)], ascii(int(1, 5))], ]); -describeOp("text_to_int", [ +describeOp("dec_to_int", [ [[bool], "error"], [[int()], "error"], [[text()], "error"], @@ -627,58 +603,68 @@ describeOp("bool_to_int", [ [[bool], int(0, 1)], ]); -describeOp("int_to_text_byte", [ +describeOp("char[byte]", [ [[text()], "error"], [[int(0)], "error"], [[int(0, 255)], text(int(1, 1))], [[int(0, 127)], ascii(int(1, 1))], ]); -describeOp("int_to_codepoint", [ +describeOp("char[codepoint]", [ [[text()], "error"], [[int(0)], "error"], [[int(0, 0x10ffff)], text(int(1, 1))], [[int(0, 127)], ascii(int(1, 1))], ]); -describeOp("list_length", [ +describeOp("size[List]", [ [[list(int()), int()], "error"], [[array(int(), 10)], "error"], [[list(int())], int(0, (1n << 31n) - 1n)], ]); -describeOp("text_codepoint_length", [ +describeOp("size[codepoint]", [ [[list(int())], "error"], [[text(int(20, 58))], int(20, 58)], [[ascii(int(20, 58))], int(20, 58)], ]); -describeOp("text_byte_length", [ +describeOp("size[byte]", [ [[list(int())], "error"], [[text(int(20, 58))], int(20, 4 * 58)], [[ascii(int(20, 58))], int(20, 58)], ]); -describeOp("text_split_whitespace", [ +describeOp("split_whitespace", [ [[list(text())], "error"], [[text(58)], list(text(58))], ]); -describeOp("sorted", [ +describeOp("sorted[Int]", [ [[array(text(), 5)], "error"], [[set(text())], "error"], [[table(text(), text())], "error"], [[text()], "error"], [[list(int())], list(int())], - [[list(text())], list(text())], + [[list(asciiType)], "error"], +]); + +describeOp("sorted[Ascii]", [ + [[array(text(), 5)], "error"], + [[set(text())], "error"], + [[table(text(), text())], "error"], + [[text()], "error"], + [[list(int())], "error"], + [[list(asciiType)], list(asciiType)], + [[list(text())], "error"], ]); -describeOp("text_byte_reversed", [ +describeOp("reversed[byte]", [ [[list(text())], "error"], [[text()], text()], ]); -describeOp("text_codepoint_reversed", [ +describeOp("reversed[codepoint]", [ [[list(text())], "error"], [[text()], text()], ]); @@ -693,7 +679,7 @@ describeOp("argc", [ [[], int(0, 2 ** 31 - 1)], ]); -describeOp("text_replace", [ +describeOp("replace", [ [[text(), text()], "error"], [[text(), text(), text()], "error"], [[text(), text(int(1)), text()], text()], @@ -702,43 +688,41 @@ describeOp("text_replace", [ [[text(58), text(int(1)), text(58)], text(58 * 58)], ]); -describeOp("text_get_codepoint_slice", [ +describeOp("slice[codepoint]", [ [[text(), int(0)], "error"], [[text(), int(), int()], "error"], [[text(), int(0), int(0)], text()], [[text(58), int(0), int(0)], text(58)], [[text(), int(0), int(0, 58)], text(58)], - [[text(), int(30, 200), int(0, 58)], text(28)], ]); -describeOp("text_get_byte_slice", [ +describeOp("slice[byte]", [ [[text(), int(0)], "error"], [[text(), int(), int()], "error"], [[text(), int(0), int(0)], text()], [[text(58), int(0), int(0)], text(58)], [[text(), int(0), int(0, 58)], text(58)], - [[text(), int(30, 200), int(0, 58)], text(28)], ]); -describeOp("array_set", [ +describeOp("set_at[Array]", [ [[array(int(), 4), text(), int()], "error"], [[array(int(), 4), int(), int()], "error"], [[array(int(), 4), int(1, 4), int()], "error"], [[array(int(-300, 300), 4), int(0, 3), text()], "error"], - [[array(int(-300, 300), 4), int(0, 3), int(10, 20)], int(-300, 300)], + [[array(int(-300, 300), 4), int(0, 3), int(10, 20)], voidType], ]); -describeOp("list_set", [ +describeOp("set_at[List]", [ [[list(int()), text(), int()], "error"], [[list(int()), int(), int()], "error"], [[list(int(-300, 300)), int(0), text()], "error"], - [[list(int(-300, 300)), int(0), int(10, 20)], int(-300, 300)], + [[list(int(-300, 300)), int(0), int(10, 20)], voidType], ]); -describeOp("table_set", [ +describeOp("set_at[Table]", [ [[table(text(), int()), int(), text()], "error"], [[table(text(5), int()), text(), int()], "error"], [[table(text(5), int(0)), text(5), int()], "error"], [[table(text(5), int(0)), text(), int(0)], "error"], - [[table(text(5), int(0)), text(4), int(100)], int(0)], + [[table(text(5), int(0)), text(4), int(100)], voidType], ]); diff --git a/src/common/getType.ts b/src/common/getType.ts index 73efad84..feb00f57 100644 --- a/src/common/getType.ts +++ b/src/common/getType.ts @@ -1,22 +1,22 @@ import { type Node, type Type, - listType, - arrayType, - integerType, + voidType, + textType as text, + listType as list, + arrayType as array, + integerType as int, + setType as set, + tableType as table, integerTypeIncludingAll, type IntegerType, type Op, isSubtype, union, toString, - voidType, - textType, type TextType, booleanType, type OpCode, - setType, - tableType, type KeyValueType, keyValueType, getArgs, @@ -38,19 +38,35 @@ import { isConstantType, constantIntegerType, type ListType, - isAssociative, op, leq, isIdent, + instantiateGenerics, + type ArrayType, + type TableType, + opCodeDefinitions, + type AnyOpCodeArgTypes, + OpCodeFrontNamesToOpCodes, + integerType, + type Rest, } from "../IR"; -import { byteLength, charLength } from "./objective"; +import { byteLength, charLength } from "./strings"; import { PolygolfError } from "./errors"; import { type Spine } from "./Spine"; import { getIdentifierType, isIdentifierReadonly } from "./symbols"; +import { stringify } from "./stringify"; + +interface TypeAndOpCode { + type: Type; + opCode?: OpCode; +} -const cachedType = new WeakMap(); +const cachedType = new WeakMap(); const currentlyFinding = new WeakSet(); -export function getType(expr: Node, context: Node | Spine): Type { +export function getTypeAndResolveOpCode( + expr: Node, + context: Node | Spine, +): TypeAndOpCode { const program = "kind" in context ? context : context.root.node; if (cachedType.has(expr)) return cachedType.get(expr)!; if (currentlyFinding.has(expr)) @@ -58,7 +74,8 @@ export function getType(expr: Node, context: Node | Spine): Type { currentlyFinding.add(expr); try { - const t = calcType(expr, program); + let t = calcTypeAndResolveOpCode(expr, program); + if ("kind" in t) t = { type: t }; currentlyFinding.delete(expr); cachedType.set(expr, t); return t; @@ -70,8 +87,14 @@ export function getType(expr: Node, context: Node | Spine): Type { throw e; } } +export function getType(expr: Node, context: Node | Spine) { + return getTypeAndResolveOpCode(expr, context).type; +} -export function calcType(expr: Node, program: Node): Type { +export function calcTypeAndResolveOpCode( + expr: Node, + program: Node, +): Type | TypeAndOpCode { // user-annotated node if (expr.type !== undefined) return expr.type; // type inference @@ -105,40 +128,6 @@ export function calcType(expr: Node, program: Node): Type { `Type error. Cannot assign ${toString(b)} to ${toString(a)}.`, ); } - case "IndexCall": { - const a = type(expr.collection); - const b = type(expr.index); - let expectedIndex: Type; - let result: Type; - switch (a.kind) { - case "Array": - expectedIndex = expr.oneIndexed - ? integerType(1, a.length) - : integerType(0, a.length - 1); - result = a.member; - break; - case "List": { - expectedIndex = integerType(expr.oneIndexed ? 1 : 0, "oo"); - result = a.member; - break; - } - case "Table": { - expectedIndex = a.key; - result = a.value; - break; - } - default: - throw new Error( - "Type error. IndexCall must be used on a collection.", - ); - } - if (isSubtype(b, expectedIndex)) { - return result; - } - throw new Error( - `Type error. Cannot index ${toString(a)} with ${toString(b)}.`, - ); - } case "Op": return getOpCodeType(expr, program); case "MutatingInfix": @@ -160,29 +149,30 @@ export function calcType(expr: Node, program: Node): Type { ); } case "Identifier": + if (expr.builtin) throw Error("Cannot calculate type of builtin."); return getIdentifierType(expr, program); case "Text": { const codepoints = charLength(expr.value); - return textType( - integerType(codepoints, codepoints), + return text( + int(codepoints, codepoints), codepoints === byteLength(expr.value), ); } case "Integer": - return integerType(expr.value, expr.value); + return int(expr.value, expr.value); case "Array": - return arrayType( + return array( expr.exprs.map(type).reduce((a, b) => union(a, b)), expr.exprs.length, ); case "List": return expr.exprs.length > 0 - ? listType(expr.exprs.map(type).reduce((a, b) => union(a, b))) - : listType("void"); + ? list(expr.exprs.map(type).reduce((a, b) => union(a, b))) + : list("void"); case "Set": return expr.exprs.length > 0 - ? setType(expr.exprs.map(type).reduce((a, b) => union(a, b))) - : setType("void"); + ? set(expr.exprs.map(type).reduce((a, b) => union(a, b))) + : set("void"); case "KeyValue": { const k = type(expr.key); const v = type(expr.value); @@ -200,11 +190,11 @@ export function calcType(expr: Node, program: Node): Type { const kTypes = kvTypes.map((x) => x.key); const vTypes = kvTypes.map((x) => x.value); return expr.kvPairs.length > 0 - ? tableType( + ? table( kTypes.reduce((a, b) => union(a, b) as any), vTypes.reduce((a, b) => union(a, b)), ) - : tableType(integerType(), "void"); + : table(int(), "void"); } throw new Error( "Programming error. Type of KeyValue nodes should always be KeyValue.", @@ -230,128 +220,122 @@ export function calcType(expr: Node, program: Node): Type { return voidType; case "OneToManyAssignment": return type(expr.expr); + case "ForRange": { + const incType = type(expr.increment); + if (!isSubtype(incType, integerType(1, Infinity))) { + throw new Error( + `Type error. Increment must be positive (got ${toString(incType)}).`, + ); + } + return voidType; + } case "If": - case "ForRange": case "While": case "ForArgv": + case "ForCLike": + case "ForEach": + case "ForEachKey": + case "ForEachPair": + case "ForDifferenceRange": return voidType; case "ImplicitConversion": { - return type(op(expr.behavesLike, expr.expr)); + return type(op.unsafe(expr.behavesLike, expr.expr)); } } - throw new Error(`Type error. Unexpected node ${expr.kind}.`); + throw new Error(`Type error. Unexpected node ${stringify(expr)}.`); } function getTypeBitNot(t: IntegerType): IntegerType { - return integerType(sub(-1n, t.high), sub(-1n, t.low)); + return int(sub(-1n, t.high), sub(-1n, t.low)); } -function getOpCodeType(expr: Op, program: Node): Type { - const types = getArgs(expr).map((x) => getType(x, program)); - function expectVariadicType( - expected: Type, - minArityOrArityCheck: number | ((x: number) => boolean) = 2, - ) { - const arityCheck = - typeof minArityOrArityCheck === "number" - ? (x: number) => x >= minArityOrArityCheck - : minArityOrArityCheck; - if ( - !arityCheck(types.length) || - types.some((x, i) => !isSubtype(x, expected)) - ) { - throw new Error( - `Type error. Operator '${ - expr.op ?? "null" - }' type error. Expected [...${toString(expected)}] but got [${types - .map(toString) - .join(", ")}].`, - ); - } - } - function expectType(...expected: Type[]) { - if ( - types.length !== expected.length || - types.some((x, i) => !isSubtype(x, expected[i])) - ) { - throw new Error( - `Type error. Operator '${ - expr.op ?? "null" - }' type error. Expected [${expected - .map(toString) - .join(", ")}] but got [${types.map(toString).join(", ")}].`, - ); - } - } - function expectGenericType( - ...expected: ( - | "Set" - | "Array" - | "List" - | "Table" - | [string, (typeArgs: Type[]) => Type] - )[] - ): Type[] { - function _throw() { - let i = 1; - const expectedS = expected.map((e) => { - switch (e) { - case "List": - case "Set": - return `(${e} T${i++})`; - case "Array": - case "Table": - return `(${e} T${i++} T${i++})`; - } - return e[0]; - }); - throw new Error( - `Type error. Operator '${ - expr.op ?? "null" - }' type error. Expected [${expectedS.join(", ")}] but got [${types - .map(toString) - .join(", ")}].`, - ); - } - if (types.length !== expected.length) _throw(); - const typeArgs: Type[] = []; - for (let i = 0; i < types.length; i++) { - const exp = expected[i]; - const got = types[i]; - if (typeof exp === "string") { - if (exp === "List" && got.kind === "List") { - typeArgs.push(got.member); - } else if (exp === "Array" && got.kind === "Array") { - typeArgs.push(got.member); - typeArgs.push(integerType(0, got.length - 1)); - } else if (exp === "Set" && got.kind === "Set") { - typeArgs.push(got.member); - } else if (exp === "Table" && got.kind === "Table") { - typeArgs.push(got.key); - typeArgs.push(got.value); - } else { - _throw(); - } +export function getInstantiatedOpCodeArgTypes(op: OpCode): Type[] { + return getGenericOpCodeArgTypes(op).map( + instantiateGenerics({ T1: int(0, 100), T2: int(0, 100) }), + ); +} + +export function getGenericOpCodeArgTypes(op: OpCode): Type[] { + const type = opCodeDefinitions[op].args; + return type.filter((x) => !("rest" in x)) as Type[]; +} + +export function expectedTypesToString( + expectedTypes: AnyOpCodeArgTypes, +): string { + return `[${expectedTypes + .map((x) => ("rest" in x ? `...${toString(x.rest)}` : toString(x))) + .join(", ")}]`; +} + +/** + * Simple algorithm for validating types of arguments of ops. Type vars are only bound by being used as an arg to a List, Array, Set or Table at a top level. + * TODO: More general unifying algo. + * @param gotTypes List of actual types provided. + * @param expectedTypes Expected types (array of types or variadic object). + * @returns True iff it is a match. + */ +function isTypeMatch(gotTypes: Type[], expectedTypes: AnyOpCodeArgTypes) { + const isVariadic = + expectedTypes.length > 0 && "rest" in expectedTypes.at(-1)!; + if (isVariadic && gotTypes.length < expectedTypes.length - 1) return false; + if (!isVariadic && expectedTypes.length !== gotTypes.length) return false; + const params: Record = {}; + const instantiate = instantiateGenerics(params); + let i = 0; + for (let got of gotTypes) { + got = instantiate(got); + let exp = instantiate( + isVariadic && i >= expectedTypes.length - 1 + ? (expectedTypes.at(-1) as Rest).rest + : (expectedTypes[i] as Type), + ); + if (exp.kind === "List" && got.kind === "List") { + if (exp.member.kind === "TypeArg" && !(exp.member.name in params)) { + params[exp.member.name] = got.member; + exp = { ...exp, member: got.member }; } - } - for (let i = 0; i < types.length; i++) { - const exp = expected[i]; - const got = types[i]; - if (typeof exp !== "string") { - const expInstantiated = exp[1](typeArgs); - if (!isSubtype(got, expInstantiated)) _throw(); + } else if (exp.kind === "Array" && got.kind === "Array") { + if (exp.member.kind === "TypeArg" && !(exp.member.name in params)) { + params[exp.member.name] = got.member; + exp = { ...exp, member: got.member }; + } + if (exp.length.kind === "TypeArg" && !(exp.length.name in params)) { + params[exp.length.name] = got.length; + exp = { ...exp, length: got.length }; + } + } else if (exp.kind === "Set" && got.kind === "Set") { + if (exp.member.kind === "TypeArg" && !(exp.member.name in params)) { + params[exp.member.name] = got.member; + exp = { ...exp, member: got.member }; + } + } else if (exp.kind === "Table" && got.kind === "Table") { + if (exp.key.kind === "TypeArg" && !(exp.key.name in params)) { + params[exp.key.name] = got.key; + exp = { ...exp, key: got.key }; + } + if (exp.value.kind === "TypeArg" && !(exp.value.name in params)) { + params[exp.value.name] = got.value; + exp = { ...exp, value: got.value }; } } - return typeArgs; + if (!isSubtype(got, exp)) return false; + i++; } + return true; +} - switch (expr.op) { +export function getOpCodeTypeFromTypes( + opCode: OpCode, + got: Type[], + skipAdditionalChecks = false, +): Type { + switch (opCode) { // binary // (num, num) => num case "gcd": { - expectType(integerType(), integerType(1)); - const [a, b] = types as [IntegerType, IntegerType]; - return integerType( + const [a, b] = got as [IntegerType, IntegerType]; + return int( 1n, min(max(abs(a.low), abs(a.high)), max(abs(b.low), abs(b.high))), ); @@ -372,67 +356,52 @@ function getOpCodeType(expr: Op, program: Node): Type { case "bit_shift_left": case "bit_shift_right": case "min": - case "max": { - const op = expr.op; - if (isAssociative(op)) { - expectVariadicType(integerType()); - } else { - expectType(integerType(), integerType()); - } - return types.reduce((a, b) => - getArithmeticType(op, a as IntegerType, b as IntegerType), + case "max": + return got.reduce((a, b) => + getArithmeticType(opCode, a as IntegerType, b as IntegerType), ); - } // (num, num) => bool case "lt": case "leq": - case "eq": - case "neq": + case "eq[Int]": + case "neq[Int]": case "geq": case "gt": - expectType(integerType(), integerType()); return booleanType; // (bool, bool) => bool case "unsafe_or": case "unsafe_and": - return booleanType; case "or": case "and": - expectVariadicType(booleanType); return booleanType; // membership - case "array_contains": - expectGenericType("Array", ["T1", (x) => x[0]]); - return booleanType; - case "list_contains": - expectGenericType("List", ["T1", (x) => x[0]]); - return booleanType; - case "table_contains_key": - expectGenericType("Table", ["T1", (x) => x[0]]); - return booleanType; - case "set_contains": - expectGenericType("Set", ["T1", (x) => x[0]]); + case "contains[Array]": + case "contains[List]": + case "contains[Table]": + case "contains[Set]": return booleanType; // collection get - case "array_get": - return expectGenericType("Array", ["T2", (x) => x[1]])[0]; - case "list_get": - return expectGenericType("List", ["0..oo", () => integerType(0)])[0]; - case "table_get": - return expectGenericType("Table", ["T1", (x) => x[0]])[1]; - case "argv_get": - expectType(integerType(0)); - return textType(); + case "at[Array]": + return (got[0] as ArrayType).member; + case "at[List]": + case "at_back[List]": + return (got[0] as ListType).member; + case "at[Table]": + return (got[0] as TableType).value; + case "at[argv]": + return text(); // other - case "list_push": - return expectGenericType("List", ["T1", (x) => x[0]])[0]; - case "list_find": - expectGenericType("List", ["T1", (x) => x[0]]); - return integerType(-1, (1n << 31n) - 1n); - case "concat": { - expectVariadicType(textType()); - const textTypes = types as TextType[]; - return textType( + case "push": + case "include": + return voidType; + case "append": + case "concat[List]": + return got[0]; + case "find[List]": + return int(-1, (1n << 31n) - 1n); + case "concat[Text]": { + const textTypes = got as TextType[]; + return text( textTypes .map((x) => x.codepointLength) .reduce((a, b) => getArithmeticType("add", a, b)), @@ -440,124 +409,92 @@ function getOpCodeType(expr: Op, program: Node): Type { ); } case "repeat": { - expectType(textType(), integerType(0)); - const [t, i] = types as [TextType, IntegerType]; - return textType( - getArithmeticType("mul", t.codepointLength, i), - t.isAscii, - ); + const [t, i] = got as [TextType, IntegerType]; + return text(getArithmeticType("mul", t.codepointLength, i), t.isAscii); } - case "text_contains": - expectType(textType(), textType()); + case "eq[Text]": + case "neq[Text]": + case "contains[Text]": return booleanType; - case "text_codepoint_find": - case "text_byte_find": - expectType(textType(), textType(integerType(1, "oo"))); - return integerType( + case "find[codepoint]": + case "find[byte]": + case "find[Ascii]": + return int( -1, sub( mul( - (types[0] as TextType).codepointLength.high, - expr.op === "text_byte_find" && !(types[0] as TextType).isAscii - ? 4n - : 1n, + (got[0] as TextType).codepointLength.high, + opCode === "find[byte]" && !(got[0] as TextType).isAscii ? 4n : 1n, ), - (types[1] as TextType).codepointLength.low, + (got[1] as TextType).codepointLength.low, ), ); - case "text_split": - expectType(textType(), textType()); - return listType(types[0]); - case "text_get_byte": - case "text_get_codepoint": - expectType(textType(), integerType(0)); - return textType(integerType(1, 1), (types[0] as TextType).isAscii); + case "split": + return list(got[0]); + case "at[byte]": + case "at[codepoint]": + case "at[Ascii]": + case "at_back[byte]": + case "at_back[codepoint]": + case "at_back[Ascii]": + return text(int(1, 1), (got[0] as TextType).isAscii); case "join": - expectType(listType(textType()), textType()); - return textType( - integerType(0, "oo"), - ((types[0] as ListType).member as TextType).isAscii && - (types[1] as TextType).isAscii, + return text( + int(0, "oo"), + ((got[0] as ListType).member as TextType).isAscii && + (got[1] as TextType).isAscii, ); case "right_align": - expectType(textType(), integerType(0)); - return textType(integerType(0, "oo"), (types[0] as TextType).isAscii); + return text(int(0, "oo"), (got[0] as TextType).isAscii); case "int_to_bin_aligned": case "int_to_hex_aligned": { - expectType(integerType(0), integerType(0)); - const t1 = types[0] as IntegerType; - const t2 = types[0] as IntegerType; + const t1 = got[0] as IntegerType; + const t2 = got[1] as IntegerType; if (isFiniteType(t1) && isFiniteType(t2)) { - return textType( + return text( integerTypeIncludingAll( BigInt( - t1.high.toString(expr.op === "int_to_bin_aligned" ? 2 : 16) - .length, + t1.high.toString(opCode === "int_to_bin_aligned" ? 2 : 16).length, ), t2.high, ), true, ); } - return textType(integerType(), true); - } - case "simplify_fraction": { - expectType(integerType(), integerType()); - const t1 = types[0] as IntegerType; - const t2 = types[1] as IntegerType; - if (isFiniteType(t1) && isFiniteType(t2)) - return textType( - integerType( - 0, - 1 + - Math.max(t1.low.toString().length, t1.high.toString().length) + - Math.max(t2.low.toString().length, t2.high.toString().length), - ), - true, - ); - return textType(); + return text(int(), true); } // unary case "abs": { - expectType(integerType()); - const t = types[0] as IntegerType; + const t = got[0] as IntegerType; if (lt(t.low, 0n) && lt(0n, t.high)) - return integerType(0, max(neg(t.low), t.high)); - return integerType( - min(abs(t.low), abs(t.high)), - max(abs(t.low), abs(t.high)), - ); + return int(0, max(neg(t.low), t.high)); + return int(min(abs(t.low), abs(t.high)), max(abs(t.low), abs(t.high))); } case "bit_not": { - expectType(integerType()); - const t = types[0] as IntegerType; + const t = got[0] as IntegerType; return getTypeBitNot(t); } case "neg": { - expectType(integerType()); - const t = types[0] as IntegerType; - return integerType(neg(t.high), neg(t.low)); + const t = got[0] as IntegerType; + return int(neg(t.high), neg(t.low)); } case "not": - expectType(booleanType); return booleanType; case "int_to_bool": - expectType(integerType()); return booleanType; - case "int_to_text": + case "int_to_dec": case "int_to_bin": case "int_to_hex": { - expectType(integerType(expr.op === "int_to_text" ? "-oo" : 0)); - const t = types[0] as IntegerType; + const t = got[0] as IntegerType; if (isFiniteType(t)) - return textType( + return text( integerTypeIncludingAll( ...[t.low, t.high, ...(typeContains(t, 0n) ? [0n] : [])].map((x) => BigInt( x.toString( - expr.op === "int_to_bin" + opCode === "int_to_bin" ? 2 - : expr.op === "int_to_hex" + : opCode === "int_to_hex" ? 16 : 10, ).length, @@ -566,152 +503,172 @@ function getOpCodeType(expr: Op, program: Node): Type { ), true, ); - return textType(integerType(1), true); + return text(int(1), true); } - case "text_to_int": { - expectType(textType(integerType(), true)); - const t = types[0] as TextType; - if (!isFiniteType(t.codepointLength)) return integerType(); - return integerType( + case "dec_to_int": { + const t = got[0] as TextType; + if (!isFiniteType(t.codepointLength)) return int(); + return int( 1n - 10n ** (t.codepointLength.high - 1n), 10n ** t.codepointLength.high - 1n, ); } case "bool_to_int": - expectType(booleanType); - return integerType(0, 1); - case "int_to_text_byte": - expectType(integerType(0, 255)); - return textType( - integerType(1n, 1n), - lt((types[0] as IntegerType).high, 128n), - ); - case "int_to_codepoint": - expectType(integerType(0, 0x10ffff)); - return textType( - integerType(1n, 1n), - lt((types[0] as IntegerType).high, 128n), - ); - case "list_length": - expectGenericType("List"); - return integerType(0, (1n << 31n) - 1n); - case "text_byte_length": { - expectType(textType()); - const codepointLength = (types[0] as TextType).codepointLength; - return integerType( + return int(0, 1); + case "char[byte]": + case "char[codepoint]": + case "char[Ascii]": + return text(int(1n, 1n), lt((got[0] as IntegerType).high, 128n)); + case "size[List]": + case "size[Set]": + case "size[Table]": + return int(0, (1n << 31n) - 1n); + case "size[byte]": { + const codepointLength = (got[0] as TextType).codepointLength; + return int( codepointLength.low, min( 1n << 31n, - mul(codepointLength.high, (types[0] as TextType).isAscii ? 1n : 4n), + mul(codepointLength.high, (got[0] as TextType).isAscii ? 1n : 4n), ), ); } - case "text_codepoint_length": - expectType(textType()); - return (types[0] as TextType).codepointLength; - case "text_split_whitespace": - expectType(textType()); - return listType(types[0]); - case "sorted": - return listType(expectGenericType("List")[0]); - case "text_byte_reversed": - case "text_codepoint_reversed": - expectType(textType()); - return types[0]; + case "size[codepoint]": + case "size[Ascii]": + return (got[0] as TextType).codepointLength; + case "split_whitespace": + return list(got[0]); + case "sorted[Int]": + case "sorted[Ascii]": + case "reversed[byte]": + case "reversed[codepoint]": + case "reversed[Ascii]": + case "reversed[List]": + return got[0]; // other case "true": case "false": - expectType(); return booleanType; - case "read_codepoint": - return textType(integerType(1, 1)); - case "read_byte": - return textType(integerType(1, 1)); - case "read_int": - return integerType(); - case "read_line": - return textType(); + case "read[codepoint]": + return text(int(1, 1)); + case "read[byte]": + return text(int(1, 1)); + case "read[Int]": + return int(); + case "read[line]": + return text(); case "argc": - expectType(); - return integerType(0, 2 ** 31 - 1); + return int(0, 2 ** 31 - 1); case "argv": - expectType(); - return listType(textType()); - case "putc": - expectType(integerType(0, 255)); + return list(text()); + case "putc[byte]": + case "putc[codepoint]": + case "putc[Ascii]": return voidType; - case "print": - case "println": - expectType(textType()); + case "print[Text]": + case "println[Text]": return voidType; - case "print_int": - case "println_int": - expectType(integerType()); + case "print[Int]": + case "println[Int]": return voidType; case "println_list_joined": - expectType(listType(textType()), textType()); return voidType; case "println_many_joined": - expectVariadicType(textType(), 1); return voidType; - case "text_replace": { - expectType(textType(), textType(integerType(1, "oo")), textType()); - const [a, c] = [types[0], types[2]] as TextType[]; - return textType( + case "replace": { + const [a, c] = [got[0], got[2]] as TextType[]; + return text( getArithmeticType("mul", a.codepointLength, c.codepointLength), a.isAscii && c.isAscii, ); } case "text_multireplace": - expectVariadicType(textType(), (x) => x > 2 && x % 2 > 0); - return textType(); - case "text_get_byte_slice": - case "text_get_codepoint_slice": { - expectType(textType(), integerType(0), integerType(0)); - const [t, i1, i2] = types as [TextType, IntegerType, IntegerType]; - const maximum = min( - t.codepointLength.high, - max(0n, sub(i2.high, i1.low)), + return text(); + case "starts_with": + case "ends_with": + return booleanType; + case "slice[byte]": + case "slice[codepoint]": + case "slice[Ascii]": + case "slice_back[byte]": + case "slice_back[codepoint]": + case "slice_back[Ascii]": { + const t = got[0] as TextType; + const start = got[1] as IntegerType; + const length = got[2] as IntegerType; + const startPlusLength = getArithmeticType("add", start, length); + if ( + skipAdditionalChecks || + !opCode.includes("back") || + isSubtype(startPlusLength, integerType(-Infinity, 0)) + ) + return text( + int(0n, min(t.codepointLength.high, length.high)), + t.isAscii, + ); + throw new Error( + `Type error. start index + length must be nonpositive, but got ${toString( + startPlusLength, + )}.`, ); - return textType(integerType(0n, maximum), t.isAscii); } - case "text_get_codepoint_to_int": - expectType(textType(), integerType(0)); - return integerType(0, (types[0] as TextType).isAscii ? 127 : 0x10ffff); - case "text_get_byte_to_int": - expectType(textType(), integerType(0)); - return integerType(0, (types[0] as TextType).isAscii ? 127 : 255); - case "codepoint_to_int": - expectType(textType(integerType(1, 1))); - return integerType(0, (types[0] as TextType).isAscii ? 127 : 0x10ffff); - case "text_byte_to_int": - expectType(textType(integerType(1, 1))); - return integerType(0, (types[0] as TextType).isAscii ? 127 : 255); - case "array_set": - return expectGenericType( - "Array", - ["T2", (x) => x[1]], - ["T1", (x) => x[0]], - )[0]; - case "list_set": - return expectGenericType( - "List", - ["0..oo", () => integerType(0)], - ["T1", (x) => x[0]], - )[0]; - case "table_set": - return expectGenericType( - "Table", - ["T1", (x) => x[0]], - ["T2", (x) => x[1]], - )[1]; - case null: + case "slice[List]": + return got[0]; + case "slice_back[List]": { + const start = got[1] as IntegerType; + const length = got[2] as IntegerType; + const startPlusLength = getArithmeticType("add", start, length); + if ( + skipAdditionalChecks || + isSubtype(startPlusLength, integerType(-Infinity, 0)) + ) + return got[0]; throw new Error( - "Cannot determine type based on null opcode - this is most likely a programming error - a plugin introduced a node missing both an opcode and a type annotation.", + `Type error. start index + length must be nonpositive, but got ${toString( + startPlusLength, + )}.`, ); + } + case "ord_at[codepoint]": + case "ord_at_back[codepoint]": + return int(0, (got[0] as TextType).isAscii ? 127 : 0x10ffff); + case "ord_at[byte]": + case "ord_at[Ascii]": + case "ord_at_back[byte]": + case "ord_at_back[Ascii]": + return int(0, (got[0] as TextType).isAscii ? 127 : 255); + case "ord[codepoint]": + return int(0, (got[0] as TextType).isAscii ? 127 : 0x10ffff); + case "ord[byte]": + case "ord[Ascii]": + return int(0, (got[0] as TextType).isAscii ? 127 : 255); + case "set_at[Array]": + case "set_at[List]": + case "set_at_back[List]": + case "set_at[Table]": + return voidType; } } +function getOpCodeType(expr: Op, program: Node): TypeAndOpCode { + const got = getArgs(expr).map((x) => getType(x, program)); + const opCodes = OpCodeFrontNamesToOpCodes[expr.op]; + + const opCode = opCodes.find((opCode) => + isTypeMatch(got, opCodeDefinitions[opCode].args), + ); + + if (opCode === undefined) { + throw new Error( + `Type error. Operator '${expr.op}' type error. Expected ${opCodes + .map((x) => expectedTypesToString(opCodeDefinitions[x].args)) + .join(" or ")} but got [${got.map(toString).join(", ")}].`, + ); + } + + return { type: getOpCodeTypeFromTypes(opCode, got), opCode }; +} + export function getArithmeticType( op: OpCode, a: IntegerType, // left argument @@ -719,13 +676,13 @@ export function getArithmeticType( ): IntegerType { switch (op) { case "min": - return integerType(min(a.low, b.low), min(a.high, b.high)); + return int(min(a.low, b.low), min(a.high, b.high)); case "max": - return integerType(max(a.low, b.low), max(a.high, b.high)); + return int(max(a.low, b.low), max(a.high, b.high)); case "add": - return integerType(add(a.low, b.low), add(a.high, b.high)); + return int(add(a.low, b.low), add(a.high, b.high)); case "sub": - return integerType(sub(a.low, b.high), sub(a.high, b.low)); + return int(sub(a.low, b.high), sub(a.high, b.low)); case "mul": { // Extreme values of a product arise from multiplying the extremes of the inputs. // The single case were simple multiplication of the bounds is not defined, corresponds to multiplying @@ -811,13 +768,8 @@ export function getArithmeticType( b, ); } - return integerType(); + return int(); case "pow": { - if (lt(b.low, 0n)) - throw new Error( - `Type error. Operator 'pow' expected [-oo..oo, 0..oo] but got ` + - `[${toString(a)}, ${toString(b)}].`, - ); const values: IntegerBound[] = []; // For unbounded b, the result must contain the following values: @@ -876,25 +828,25 @@ export function getArithmeticType( return getArithmeticType( "mul", a, - getArithmeticType("pow", integerType(2, 2), b), + getArithmeticType("pow", int(2, 2), b), ); case "bit_shift_right": return getArithmeticType( "div", a, - getArithmeticType("pow", integerType(2, 2), b), + getArithmeticType("pow", int(2, 2), b), ); case "bit_or": case "bit_xor": { const left = max(abs(a.low), abs(a.high)); const right = max(abs(b.low), abs(b.high)); if (isFiniteBound(left) && isFiniteBound(right)) { - const larger = lt(left, right) ? left : right; + const larger = lt(left, right) ? right : left; const lim = 2n ** BigInt(larger.toString(2).length); - if (lt(-1n, a.low) && lt(-1n, b.low)) return integerType(0n, lim); - return integerType(neg(lim), lim); + if (lt(-1n, a.low) && lt(-1n, b.low)) return int(0n, lim); + return int(neg(lim), lim); } - return integerType(); + return int(); } } throw new Error(`Type error. Unknown opcode. ${op ?? "null"}`); @@ -910,7 +862,7 @@ export function getCollectionTypes(expr: Node, program: Node): Type[] { case "Table": return [exprType.key, exprType.value]; case "text": - return [textType(integerType(1, 1), exprType.isAscii)]; + return [text(int(1, 1), exprType.isAscii)]; } throw new Error("Type error. Node is not a collection."); } @@ -933,5 +885,5 @@ function getIntegerTypeRem(a: IntegerType, b: IntegerType): IntegerType { return constantIntegerType(a.low % b.low); } const m = max(abs(b.low), abs(b.high)); - return integerType(lt(a.low, 0n) ? neg(m) : 0n, m); + return int(lt(a.low, 0n) ? neg(m) : 0n, m); } diff --git a/src/common/objective.ts b/src/common/objective.ts index 78e2a192..182dc1b0 100644 --- a/src/common/objective.ts +++ b/src/common/objective.ts @@ -1,4 +1,5 @@ import { type CompilationOptions, type CompilationResult } from "./compile"; +import { byteLength, charLength } from "./strings"; export type Objective = "bytes" | "chars"; export type ObjectiveFunc = (x: string | null) => number; @@ -9,70 +10,6 @@ export function getObjectiveFunc(options: CompilationOptions): ObjectiveFunc { return options.objective; } -// This is what code.golf uses for char scoring -// https://github.com/code-golf/code-golf/blob/13733cfd472011217031fb9e733ae9ac177b234b/js/_util.ts#L7 -export const charLength = (str: string | null) => { - if (str === null) return Infinity; - let i = 0; - let len = 0; - - while (i < str.length) { - const value = str.charCodeAt(i++); - - if (0xd800 <= value && value <= 0xdbff && i < str.length) { - // It's a high surrogate, and there is a next character. - const extra = str.charCodeAt(i++); - - // Low surrogate. - if ((extra & 0xfc00) === 0xdc00) { - len++; - } else { - // It's an unmatched surrogate; only append this code unit, in - // case the next code unit is the high surrogate of a - // surrogate pair. - len++; - i--; - } - } else { - len++; - } - } - - return len; -}; - -export const codepoints = (str: string) => { - let i = 0; - const result: number[] = []; - - while (i < str.length) { - const value = str.charCodeAt(i++); - - if (value >= 0xd800 && value <= 0xdbff && i < str.length) { - // It's a high surrogate, and there is a next character. - const extra = str.charCodeAt(i++); - - // Low surrogate. - if ((extra & 0xfc00) === 0xdc00) { - result.push((((value - 0xd800) << 10) ^ (extra - 0xdc00)) + 0x10000); - } else { - // It's an unmatched surrogate; only append this code unit, in - // case the next code unit is the high surrogate of a - // surrogate pair. - result.push(value); - i--; - } - } else { - result.push(value); - } - } - - return result; -}; - -export const byteLength = (x: string | null) => - x === null ? Infinity : Buffer.byteLength(x, "utf-8"); - function isError(x: any): x is Error { return x instanceof Error; } diff --git a/src/common/strings.ts b/src/common/strings.ts new file mode 100644 index 00000000..b155f2be --- /dev/null +++ b/src/common/strings.ts @@ -0,0 +1,92 @@ +// This is what code.golf uses for char scoring +// https://github.com/code-golf/code-golf/blob/13733cfd472011217031fb9e733ae9ac177b234b/js/_util.ts#L7 +export const charLength = (str: string | null) => { + if (str === null) return Infinity; + let i = 0; + let len = 0; + + while (i < str.length) { + const value = str.charCodeAt(i++); + + if (0xd800 <= value && value <= 0xdbff && i < str.length) { + // It's a high surrogate, and there is a next character. + const extra = str.charCodeAt(i++); + + // Low surrogate. + if ((extra & 0xfc00) === 0xdc00) { + len++; + } else { + // It's an unmatched surrogate; only append this code unit, in + // case the next code unit is the high surrogate of a + // surrogate pair. + len++; + i--; + } + } else { + len++; + } + } + + return len; +}; + +export const codepoints = (str: string) => { + let i = 0; + const result: number[] = []; + + while (i < str.length) { + const value = str.charCodeAt(i++); + + if (value >= 0xd800 && value <= 0xdbff && i < str.length) { + // It's a high surrogate, and there is a next character. + const extra = str.charCodeAt(i++); + + // Low surrogate. + if ((extra & 0xfc00) === 0xdc00) { + result.push((((value - 0xd800) << 10) ^ (extra - 0xdc00)) + 0x10000); + } else { + // It's an unmatched surrogate; only append this code unit, in + // case the next code unit is the high surrogate of a + // surrogate pair. + result.push(value); + i--; + } + } else { + result.push(value); + } + } + + return result; +}; + +export const chars = (str: string) => { + let i = 0; + const result: string[] = []; + + while (i < str.length) { + const value = str.charCodeAt(i); + + if (value >= 0xd800 && value <= 0xdbff && i + 1 < str.length) { + // It's a high surrogate, and there is a next character. + const extra = str.charCodeAt(i + 1); + + // Low surrogate. + if ((extra & 0xfc00) === 0xdc00) { + result.push(str.slice(i, i + 2)); + i += 2; + } else { + // It's an unmatched surrogate; only append this code unit, in + // case the next code unit is the high surrogate of a + // surrogate pair. + result.push(str[i++]); + } + } else { + result.push(str[i++]); + } + } + + return result; +}; + +export const byteLength = (x: string | null) => + x === null ? Infinity : Buffer.byteLength(x, "utf-8"); diff --git a/src/common/symbols.ts b/src/common/symbols.ts index fc6bdab9..16e68253 100644 --- a/src/common/symbols.ts +++ b/src/common/symbols.ts @@ -318,9 +318,9 @@ function getDirectWriteFragments(node: Node): PathFragment[] { function getDirectPolygolfWriteFragments(node: Op): number[] { switch (node.op) { - case "array_set": - case "list_set": - case "table_set": + case "set_at[Array]": + case "set_at[List]": + case "set_at[Table]": return [0]; } return []; @@ -333,7 +333,7 @@ export function hasSideEffect(spine: Spine): boolean { export function hasDirectSideEffect(node: Node, spine: Spine) { try { return ( - isOp("read_byte", "read_codepoint", "read_line", "read_int")(node) || + isOp("read[byte]", "read[codepoint]", "read[line]", "read[Int]")(node) || getType(node, spine).kind === "void" ); } catch { @@ -342,11 +342,11 @@ export function hasDirectSideEffect(node: Node, spine: Spine) { } export function readsFromStdin(node: Node): boolean { - return isOp("read_byte", "read_codepoint", "read_line", "read_int")(node); + return isOp("read[byte]", "read[codepoint]", "read[line]", "read[Int]")(node); } export function readsFromArgv(node: Node): boolean { - return node.kind === "ForArgv" || isOp("argv", "argv_get")(node); + return node.kind === "ForArgv" || isOp("argv", "at[argv]")(node); } export function readsFromInput(node: Node): boolean { diff --git a/src/cover/index.ts b/src/cover/index.ts new file mode 100644 index 00000000..5f047a0c --- /dev/null +++ b/src/cover/index.ts @@ -0,0 +1,221 @@ +import { getInstantiatedOpCodeArgTypes, getType } from "../common/getType"; +import type { Language } from "../common/Language"; +import { + annotate, + assignment, + builtin, + integerType, + op, + type Node, + int, + id, + func, + ifStatement, + forRange, + whileLoop, + forArgv, + conditional, + list, + array, + set, + table, + keyValue, + anyInt, + type Type, + booleanType, + text, + getLiteralOfType, + OpCodes, + OpCodesUser, + isSubtype, +} from "../IR"; +import languages from "../languages/languages"; +import { isCompilable } from "../common/compile"; +import asTable from "as-table"; +import { mapObjectValues } from "../common/arrays"; +import yargs from "yargs"; + +const options = yargs() + .options({ + all: { + alias: "a", + description: "Print rows that are all true & backend only opcodes", + type: "boolean", + }, + }) + .parseSync(process.argv.slice(2)); + +/** + * To find out whether certain node is compilable in a given language, we must be sure that all its children are compilable. + * This aims at providing basic compilable building blocks. + */ +interface LangCoverConfig { + expr: (x?: Type, preferBuiltin?: boolean) => Node; // returns any node of given type (or 0..0) + stmt: (x?: Node) => Node; // returns any node of type void containing given Node (or any) +} + +const langs = languages.filter( + (x) => x.name !== "Polygolf" && x.name !== "Text", +) as (Language & LangCoverConfig)[]; + +let nextBuiltinState = -1; +function nextBuiltin(x: Type) { + if (x.kind === "integer") + x = isSubtype(x, integerType(-Infinity, 0)) + ? integerType(-64, -1) + : integerType(0, 64); + nextBuiltinState = (nextBuiltinState + 1) % 26; + return annotate(builtin(String.fromCharCode(65 + nextBuiltinState)), x); +} + +for (const lang of langs) { + const compilesAssignment = isCompilable(assignment(id("x"), int(0)), lang); + const compilesPrintInt = isCompilable(op["print[Int]"](int(0)), lang); + const compilesPrint = isCompilable(op["print[Text]"](text("x")), lang); + + lang.stmt = function (x: Node | undefined) { + x ??= compilesPrintInt ? int(0) : text("x"); + const type = getType(x, x); + if (compilesPrint && type.kind === "text") return op["print[Text]"](x); + if (compilesPrintInt && type.kind === "integer") return op["print[Int]"](x); + if (compilesAssignment) return assignment(id("x"), x); + return x; + }; + + lang.expr = function (x: Type = integerType(1, 1), preferBuiltin = false) { + const literal = getLiteralOfType(x, true); + return !preferBuiltin && isCompilable(literal, lang) + ? literal + : nextBuiltin(x); + }; +} + +type Table = Record>; +type CoverTableRecipe = Record Node>; + +function printTable(name: string, x: Table) { + console.log( + "\n" + + asTable([ + { + [name]: "", + ...mapObjectValues( + Object.values(x)[0], + (v, k) => + `${Math.floor( + (100 * + Object.values(x) + .map((x) => x[k]) + .filter((x) => x === true).length) / + Object.values(x).length, + )}%`, + ), + }, + ...Object.entries(x) + .filter( + ([k, v]) => + options.all === true || + Object.values(v).some((x, _, a) => x !== a[0]), + ) + .map(([k, v]) => ({ + [name]: k.padEnd(25), + ...mapObjectValues(v, (v2) => + v2 === true + ? "✔️" + : v2 === false + ? "❌" + : v2 === undefined + ? "" + : v2, + ), + })), + ]).replaceAll("❌ ", "❌"), // no table generating library I tried was able to align ❌ correctly + ); +} + +function runCoverTableRecipe(recipe: CoverTableRecipe): Table { + return mapObjectValues(recipe, (f) => + Object.fromEntries( + langs.map((lang) => [ + lang.extension.padEnd(3), + isCompilable(f(lang), lang), + ]), + ), + ); +} + +const features: CoverTableRecipe = { + assignment: (lang) => assignment(id("x"), lang.expr()), + builtin: (lang) => lang.stmt(nextBuiltin(integerType(0, 0))), + discard: (lang) => lang.expr(), + bigint: (lang) => lang.stmt(int(10n ** 40n)), + if: (lang) => ifStatement(lang.expr(booleanType), lang.stmt(), lang.stmt()), + for: (lang) => + forRange( + id("x"), + lang.expr(integerType(4, 4)), + lang.expr(integerType(10, 10)), + int(1), + lang.stmt(), + ), + "for with step": (lang) => + forRange( + id("x"), + lang.expr(integerType(4, 4)), + lang.expr(integerType(10, 10)), + lang.expr(integerType(3, 3)), + lang.stmt(), + ), + while: (lang) => whileLoop(lang.expr(booleanType), lang.stmt()), + for_argv: (lang) => forArgv(id("x"), 100, lang.stmt()), + conditional: (lang) => + conditional(lang.expr(booleanType), lang.expr(), lang.expr()), + unsafe_conditional: (lang) => + conditional(lang.expr(booleanType), lang.expr(), lang.expr()), + any_int: () => anyInt(10n, 20n), + list: (lang) => list([lang.expr()]), + array: (lang) => array([lang.expr()]), + set: (lang) => set([lang.expr()]), + table: (lang) => table([keyValue(lang.expr(), lang.expr())]), + function: () => func(["x", "y"], id("x")), +}; + +const opCodes: CoverTableRecipe = Object.fromEntries( + OpCodesUser.map((opCode) => [ + opCode, + (lang) => + lang.stmt( + op.unsafe( + opCode, + ...getInstantiatedOpCodeArgTypes(opCode).map((x) => + lang.expr(x, opCode.startsWith("set_") || opCode === "push"), + ), + ), + ), + ]), +); + +printTable("Features", runCoverTableRecipe(features)); +printTable("OpCodes", runCoverTableRecipe(opCodes)); + +if (options.all === true) { + printTable( + "Backend OpCodes", + runCoverTableRecipe( + Object.fromEntries( + OpCodes.filter((x) => !OpCodesUser.includes(x as any)).map((opCode) => [ + opCode, + (lang) => + lang.stmt( + op.unsafe( + opCode, + ...getInstantiatedOpCodeArgTypes(opCode).map((x) => + lang.expr(x), + ), + ), + ), + ]), + ), + ), + ); +} diff --git a/src/docs-gen/index.ts b/src/docs-gen/index.ts new file mode 100644 index 00000000..94bb4f62 --- /dev/null +++ b/src/docs-gen/index.ts @@ -0,0 +1,46 @@ +import { groupby } from "../common/arrays"; +import { + expectedTypesToString, + getGenericOpCodeArgTypes, + getOpCodeTypeFromTypes, +} from "../common/getType"; +import { + type OpCode, + OpCodesUser, + opCodeDefinitions, + toString, + userName, + opCodeDescriptions, +} from "../IR"; +import fs from "fs"; +import path from "path"; + +let result = `# OpCodes +Hover opcode name to see a description. + +| Alias | Full name | Input | Output | +|-------|-----------|-------|--------| +`; + +function getOpCodeOutputType(opCode: OpCode) { + try { + return toString( + getOpCodeTypeFromTypes(opCode, getGenericOpCodeArgTypes(opCode), true), + ); + } catch { + return "?"; + } +} + +for (const [alias, opCodes] of groupby(OpCodesUser, userName).entries()) { + result += `| ${alias.replace("|", "\\|")} | ${opCodes + .map((x) => `[${x}](## ${JSON.stringify(opCodeDescriptions[x])})`) + .join("
")} | ${opCodes + .map((x) => expectedTypesToString(opCodeDefinitions[x].args)) + .join("
")} | ${opCodes.map(getOpCodeOutputType).join("
")} |\n`; +} + +fs.writeFileSync( + path.join(process.cwd(), "docs", "opcodes.generated.md"), + result, +); diff --git a/src/frontend/grammar.ne b/src/frontend/grammar.ne index c1aaab44..bdffd742 100644 --- a/src/frontend/grammar.ne +++ b/src/frontend/grammar.ne @@ -58,7 +58,7 @@ callee -> builtin {% id %} integer -> %integer {% d => refSource(int(d[0]), d[0]) %} variable -> %variable {% d => refSource(userIdentifier(d[0]), d[0]) %} -builtin -> (%builtin | "argv_get") {% d => refSource(identifier(d[0][0].value, true), d[0][0]) %} +builtin -> (%builtin | "for_argv") {% d => refSource(identifier(d[0][0].value, true), d[0][0]) %} opalias -> (%opalias | "..") {% d => refSource(identifier(d[0][0].value, true), d[0][0]) %} nullary -> %nullary {% d => refSource(sexpr(identifier(d[0].value, true), []), d[0]) %} string -> %string {% d => refSource(text(JSON.parse(d[0])), d[0]) %} diff --git a/src/frontend/lexer.ts b/src/frontend/lexer.ts index ec5296f7..f8390f37 100644 --- a/src/frontend/lexer.ts +++ b/src/frontend/lexer.ts @@ -1,3 +1,4 @@ +import { NullaryOpCodes, infixableOpCodeNames } from "../IR"; import moo from "moo"; const tokenTable = { @@ -5,26 +6,19 @@ const tokenTable = { /0|-?[1-9]\d*(?:[eE][1-9]\d*)?|-?0x[1-9a-fA-F][\da-fA-F]*|-?0b1[01]*/, string: /"(?:\\.|[^"])*"/, variable: /\$\w+/, - type: /[A-Z][a-z]*/, - argv_get: "argv_get", - nullary: [ - "argv", - "argc", - "true", - "false", - "read_codepoint", - "read_byte", - "read_int", - "read_line", - ], + for_argv: "for_argv", + nullary: NullaryOpCodes, ninf: ["-oo", "-∞"], pinf: ["oo", "∞"], variant: "/", - opalias: - "<- + - * ^ & | ~ >> << == != <= < >= > => # mod rem div trunc_div".split( - " ", - ), - builtin: /[a-z0-9_]+/, + opalias: [ + ...infixableOpCodeNames, + ...infixableOpCodeNames.map((x) => x + "<-"), + "<-", + "=>", + ], + builtin: /[a-z0-9_]+(?:\[[A-Za-z][a-z]*\])?/, + type: /[A-Z][a-z]*/, lparen: "(", rparen: ")", lbrace: "{", diff --git a/src/frontend/parse-emit.test.ts b/src/frontend/parse-emit.test.ts index 9eeb75b7..d667835f 100644 --- a/src/frontend/parse-emit.test.ts +++ b/src/frontend/parse-emit.test.ts @@ -2,7 +2,7 @@ import { normalize } from "@/common/compile"; describe("Restricted nodes: parse - emit match", () => { for (const t of [ - `implicit_conversion "text_to_int" "1";`, + `implicit_conversion "dec_to_int" "1";`, `var_declaration $x:Int;`, `func $x $x;`, `var_declaration_with_assignment ($x:Int <- 0);`, @@ -11,9 +11,7 @@ describe("Restricted nodes: parse - emit match", () => { `one_to_many_assignment {$x; $y} "x";`, `mutating_infix "+" $x $y;`, `index_call $x $y;`, - `index_call_one_indexed $x $y;`, `range_index_call $x $y $z $w;`, - `range_index_call_one_indexed $x $y $z $w;`, `method_call $o "name" $x $y;`, `property_call $o "name";`, `infix "name" $x $y;`, diff --git a/src/frontend/parse.test.ts b/src/frontend/parse.test.ts index 31a7b7cd..ad17b6a6 100644 --- a/src/frontend/parse.test.ts +++ b/src/frontend/parse.test.ts @@ -25,7 +25,7 @@ import parse from "./parse"; function testStmtParse(desc: string, str: string, output: Node) { test(desc, () => { - expect(stringify(parse(str, false))).toEqual(stringify(output)); + expect(stringify(parse(str, false).node)).toEqual(stringify(output)); }); } @@ -49,8 +49,8 @@ describe("Parse literals", () => { }); describe("Parse s-expressions", () => { - expectExprParse("true nullary op", "true", op("true")); - expectExprParse("argv nullary op", "argv", op("argv")); + expectExprParse("true nullary op", "true", op.true); + expectExprParse("argv nullary op", "argv", op.argv); expectExprParse( "user function", "($f 1 2)", @@ -61,29 +61,29 @@ describe("Parse s-expressions", () => { "($f $x $y)", functionCall(id("f"), id("x"), id("y")), ); - expectExprParse("add", "(add $x $y)", op("add", id("x"), id("y"))); - expectExprParse("add infix", "($x + $y)", op("add", id("x"), id("y"))); - expectExprParse("mod infix", "($x mod $y)", op("mod", id("x"), id("y"))); - expectExprParse("or", "(or $x $y)", op("or", id("x"), id("y"))); - expectExprParse("println", "(println $x)", print(id("x"), true)); - expectExprParse("print", "(print $x)", print(id("x"), false)); + expectExprParse("add", "(add $x $y)", op.add(id("x"), id("y"))); + expectExprParse("add infix", "($x + $y)", op.add(id("x"), id("y"))); + expectExprParse("mod infix", "($x mod $y)", op.mod(id("x"), id("y"))); + expectExprParse("or", "(or $x $y)", op.or(id("x"), id("y"))); + expectExprParse("println[Text]", "(println[Text] $x)", print(id("x"), true)); + expectExprParse("print[Text]", "(print[Text] $x)", print(id("x"), false)); expectExprParse("assign", "(assign $x 5)", assignment(id("x"), int(5n))); expectExprParse("assign infix", "($x <- 5)", assignment(id("x"), int(5n))); expectExprParse("list", "(list 1 2 3)", list([int(1n), int(2n), int(3n)])); expectExprParse( "+", "(+ $x $y $z $w)", - op("add", op("add", op("add", id("x"), id("y")), id("z")), id("w")), + op.add(id("x"), id("y"), id("z"), id("w")), ); expectExprParse( "..", - "(.. $x $y $z)", - op("concat", op("concat", id("x"), id("y")), id("z")), + "(concat[Text] $x $y $z)", + op["concat[Text]"](id("x"), id("y"), id("z")), ); - expectExprParse("- as neg", "(- $x)", op("neg", id("x"))); - expectExprParse("- as sub", "(- $x $y)", op("sub", id("x"), id("y"))); - expectExprParse("~ as bitnot", "(~ $x)", op("bit_not", id("x"))); - expectExprParse("~ as bitxor", "(~ $x $y)", op("bit_xor", id("x"), id("y"))); + expectExprParse("- as neg", "(- $x)", op.neg(id("x"))); + expectExprParse("- as sub", "(- $x $y)", op.sub(id("x"), id("y"))); + expectExprParse("~ as bitnot", "(~ $x)", op.bit_not(id("x"))); + expectExprParse("~ as bitxor", "(~ $x $y)", op.bit_xor(id("x"), id("y"))); }); describe("Parse annotations", () => { @@ -105,18 +105,18 @@ describe("Parse annotations", () => { describe("Parse statements", () => { testStmtParse( "comment", - `%one\nprintln 58;%two\n%println -3;`, + `%one\nprintln[Text] 58;%two\n%println[Text] -3;`, print(int(58n), true), ); testStmtParse("infix assignment", "$x <- 5;", assignment(id("x"), int(5n))); testStmtParse( "if", - "if $x (println $y);", + "if $x (println[Text] $y);", ifStatement(id("x"), print(id("y"), true)), ); testStmtParse( "forRange", - "for $x 1 20 1 (println $x);", + "for $x 1 20 1 (println[Text] $x);", forRange(id("x"), int(1n), int(20n), int(1n), print(id("x"), true)), ); }); @@ -124,7 +124,7 @@ describe("Parse statements", () => { describe("Parse variants", () => { testStmtParse( "Two variants", - `{ println $x; / print $x; print "\\n"; }`, + `{ println[Text] $x; / print[Text] $x; print[Text] "\\n"; }`, variants([ print(id("x"), true), block([print(id("x"), false), print(text("\n"), false)]), @@ -132,7 +132,7 @@ describe("Parse variants", () => { ); testStmtParse( "Three variants", - `{ println $x; / print $x; print "\\n"; / print $x; print "\\n"; }`, + `{ println[Text] $x; / print[Text] $x; print[Text] "\\n"; / print[Text] $x; print[Text] "\\n"; }`, variants([ print(id("x"), true), block([print(id("x"), false), print(text("\n"), false)]), @@ -141,7 +141,7 @@ describe("Parse variants", () => { ); testStmtParse( "Node variants", - `println { 0 / 1 };`, + `println[Text] { 0 / 1 };`, print(variants([int(0n), int(1n)]), true), ); }); @@ -157,3 +157,17 @@ describe("Parse unambiguously", () => { variants([variants([assignment(id("a"), int(0n))])]), ); }); + +describe("Parse indexing asignment", () => { + testStmtParse( + `Rewrite to set_at`, + ` + $x <- (list 2 3); + ($x @ 0) <- 3; + `, + block([ + assignment("x", list([int(2), int(3)])), + op.unsafe("set_at" as any, id("x"), int(0), int(3)), + ]), + ); +}); diff --git a/src/frontend/parse.ts b/src/frontend/parse.ts index 93000aca..db0e2a84 100644 --- a/src/frontend/parse.ts +++ b/src/frontend/parse.ts @@ -27,9 +27,6 @@ import { set, table, type KeyValue, - isOpCode, - isBinary, - arity, functionType, func, conditional, @@ -37,8 +34,6 @@ import { array, toString, forArgv, - isAssociative, - isFrontend, implicitConversion, varDeclaration, varDeclarationWithAssignment, @@ -61,29 +56,64 @@ import { propertyCall, isText, anyInt, - isIntLiteral, + isInt, isIdent, postfix, type Text, functionDefinition, scanningMacroCall, + type OpCodeFrontName, + OpCodesUser, + OpCodeFrontNamesToOpCodes, + OpCodeFrontNames, + opCodeDefinitions, + matchesOpCodeArity, + isOp, + userName, } from "../IR"; import grammar from "./grammar"; let restrictedFrontend = true; -export function sexpr(callee: Identifier, args: readonly Node[]): Node { - if (!callee.builtin) { - return functionCall(callee, args); +let warnings: Error[] = []; + +export function sexpr( + calleeIdent: Identifier, + args: readonly Node[], + callee: string = calleeIdent.name, +): Node { + if (!calleeIdent.builtin) { + return functionCall(calleeIdent, args); + } + if (callee === "<-") callee = "assign"; + if (callee === "=>") callee = "key_value"; + if (callee.endsWith("<-")) { + return sexpr( + calleeIdent, + [args[0], sexpr(calleeIdent, args, callee.slice(0, callee.length - 2))], + "<-", + ); + } + if (callee in deprecatedAliases) { + warnings.push( + new PolygolfError( + `Deprecated alias used: ${callee}. Use ${deprecatedAliases[callee]} ${ + deprecatedAliases[callee] === userName(deprecatedAliases[callee]) + ? "" + : `or ${userName(deprecatedAliases[callee])} ` + }instead.`, + calleeIdent.source, + ), + ); + callee = deprecatedAliases[callee]; } - const opCode = canonicalOp(callee.name, args.length); function expectArity(low: number, high: number = low) { if (args.length < low || args.length > high) { throw new PolygolfError( - `Syntax error. Invalid argument count in application of ${opCode}: ` + + `Syntax error. Invalid argument count in application of ${callee}: ` + `Expected ${low}${low === high ? "" : ".." + String(high)} but got ${ args.length }.`, - callee.source, + calleeIdent.source, ); } } @@ -95,7 +125,7 @@ export function sexpr(callee: Identifier, args: readonly Node[]): Node { ); } function assertInteger(e: Node): asserts e is Integer { - if (!isIntLiteral()(e)) + if (!isInt()(e)) throw new PolygolfError( `Syntax error. Expected integer literal, but got ${e.kind}`, e.source, @@ -108,7 +138,7 @@ export function sexpr(callee: Identifier, args: readonly Node[]): Node { for (const x of e) { if (x.kind !== "KeyValue") throw new PolygolfError( - `Syntax error. Application ${opCode} requires list of key-value pairs as argument`, + `Syntax error. Application ${callee} requires list of key-value pairs as argument`, x.source, ); } @@ -131,8 +161,16 @@ export function sexpr(callee: Identifier, args: readonly Node[]): Node { e.source, ); } + if ( + callee === "assign" && + isOp("@" as any)(args[0]) && + args[0].args.length === 2 + ) { + callee = "set_at"; + args = [...args[0].args, args[1]]; + } - switch (opCode) { + switch (callee) { case "key_value": expectArity(2); return keyValue(args[0], args[1]); @@ -149,7 +187,7 @@ export function sexpr(callee: Identifier, args: readonly Node[]): Node { return assignment(args[0], args[1]); case "function_call": { expectArity(1, Infinity); - assertIdentifier(args[0]); + if (restrictedFrontend) assertIdentifier(args[0]); return functionCall(args[0], args.slice(1)); } case "array": @@ -165,7 +203,7 @@ export function sexpr(callee: Identifier, args: readonly Node[]): Node { case "conditional": case "unsafe_conditional": expectArity(3); - return conditional(args[0], args[1], args[2], opCode === "conditional"); + return conditional(args[0], args[1], args[2], callee === "conditional"); case "while": expectArity(2); return whileLoop(args[0], args[1]); @@ -217,7 +255,7 @@ export function sexpr(callee: Identifier, args: readonly Node[]): Node { } } if (!restrictedFrontend) - switch (opCode) { + switch (callee) { case "implicit_conversion": expectArity(2); return implicitConversion(asString(args[0]) as any, args[1]); @@ -249,19 +287,11 @@ export function sexpr(callee: Identifier, args: readonly Node[]): Node { assertIdentifier(args[1]); return mutatingInfix(asString(args[0]), args[1], args[2]); case "index_call": - case "index_call_one_indexed": expectArity(2); - return indexCall(args[0], args[1], opCode === "index_call_one_indexed"); + return indexCall(args[0], args[1]); case "range_index_call": - case "range_index_call_one_indexed": expectArity(4); - return rangeIndexCall( - args[0], - args[1], - args[2], - args[3], - opCode === "range_index_call_one_indexed", - ); + return rangeIndexCall(args[0], args[1], args[2], args[3]); case "property_call": expectArity(2); return propertyCall(args[0], asString(args[1])); @@ -280,7 +310,7 @@ export function sexpr(callee: Identifier, args: readonly Node[]): Node { case "builtin": case "id": expectArity(1); - return id(asString(args[0]), opCode === "builtin"); + return id(asString(args[0]), callee === "builtin"); case "import": expectArity(2, Infinity); return importStatement(asString(args[0]), args.slice(1).map(asString)); @@ -359,8 +389,8 @@ export function sexpr(callee: Identifier, args: readonly Node[]): Node { const body = args[args.length - 1]; assertIdentifiers(idents); const opts = { - isGlobal: opCode.includes("global"), - isExpanded: opCode.includes("expanded"), + isGlobal: callee.includes("global"), + isExpanded: callee.includes("expanded"), }; return functionDefinition(name, idents, body, opts); } @@ -370,22 +400,46 @@ export function sexpr(callee: Identifier, args: readonly Node[]): Node { return scanningMacroCall(args[0], ...args.slice(1)); } } - if (isOpCode(opCode) && (!restrictedFrontend || isFrontend(opCode))) { - if (opCode === "argv_get" && restrictedFrontend) { - assertInteger(args[0]); - } - if (isBinary(opCode)) { - expectArity(2, isAssociative(opCode) ? Infinity : 2); - return op(opCode, ...args); - } - const ar = arity(opCode); - expectArity(ar, ar === -1 ? Infinity : ar); - return op(opCode, ...args); + let matchingOpCodes = OpCodeFrontNames.includes(callee) + ? OpCodeFrontNamesToOpCodes[callee as OpCodeFrontName] + : []; + if (restrictedFrontend) { + matchingOpCodes = matchingOpCodes.filter((opCode) => + OpCodesUser.includes(opCode), + ); } - throw new PolygolfError( - `Syntax error. Unrecognized builtin: ${opCode}`, - callee.source, + if (matchingOpCodes.length < 1) { + throw new PolygolfError( + `Syntax error. Unrecognized builtin: ${callee}`, + calleeIdent.source, + ); + } + + const arityMatchingOpCodes = matchingOpCodes.filter((opCode) => + matchesOpCodeArity(opCode, args.length), ); + if (arityMatchingOpCodes.length < 1) { + throw new PolygolfError( + `Syntax error. Invalid argument count in application of ${callee}: ` + + `Expected ${matchingOpCodes + .map((opCode) => opCodeDefinitions[opCode].args) + .map((args) => + args.length > 0 && "rest" in args.at(-1)! + ? `${args.length - 1}..oo` + : `${args.length}`, + ) + .join(", ")} but got ${args.length}.`, + calleeIdent.source, + ); + } + + if (arityMatchingOpCodes.length > 1) { + // Hack! We temporarily assign the front name to the opCode field. + // It will be resolved during typecheck. + return op.unsafe(callee as OpCode, ...args); + } + + return op.unsafe(arityMatchingOpCodes[0], ...args); } function intValue(x: string): bigint { @@ -399,34 +453,6 @@ export function int(x: Token) { return integer(intValue(x.text)); } -export const canonicalOpTable: Record = { - "+": "add", - // neg, sub handled as special case in canonicalOp - "*": "mul", - "^": "pow", - "&": "bit_and", - "|": "bit_or", - "<<": "bit_shift_left", - ">>": "bit_shift_right", - // bitxor, bitnot handled as special case in canonicalOp - "==": "eq", - "!=": "neq", - "<=": "leq", - "<": "lt", - ">=": "geq", - ">": "gt", - "#": "list_length", - "..": "concat", -}; - -function canonicalOp(op: string, arity: number): string { - if (op === "<-") return "assign"; - if (op === "=>") return "key_value"; - if (op === "-") return arity < 2 ? "neg" : "sub"; - if (op === "~") return arity < 2 ? "bit_not" : "bit_xor"; - return canonicalOpTable[op] ?? op; -} - export function userIdentifier(token: Token): Identifier { const name = token.value.slice(1); return id(name, false); @@ -558,7 +584,16 @@ export function refSource(node: Node, ref?: Token | Node): Node { }; } -export default function parse(code: string, restrictFrontend = true) { +export interface ParseResult { + node: Node; + warnings: Error[]; +} + +export default function parse( + code: string, + restrictFrontend = true, +): ParseResult { + warnings = []; restrictedFrontend = restrictFrontend; const parser = new nearley.Parser(nearley.Grammar.fromCompiled(grammar)); try { @@ -605,5 +640,51 @@ export default function parse(code: string, restrictFrontend = true) { column: (lines.at(-1)?.length ?? 0) + 1, }); } - return results[0] as Node; + return { + node: results[0] as Node, + warnings, + }; } + +// TODO add more +const deprecatedAliases: Record = { + text_contains: "contains[Text]", + array_contains: "contains[Array]", + list_contains: "contains[List]", + table_contains_key: "contains[Table]", + set_contains: "contains[Set]", + argv_get: "at[argv]", + array_get: "at[Array]", + list_get: "at[List]", + table_get: "at[Table]", + text_get_byte: "at[byte]", + text_get_codepoint: "at[codepoint]", + array_set: "set_at[Array]", + list_set: "set_at[List]", + table_set: "set_at[Table]", + text_byte_find: "find[byte]", + text_codepoint_find: "find[codepoint]", + text_get_byte_to_int: "ord_at[byte]", + text_get_codepoint_to_int: "ord_at[codepoint]", + text_byte_to_int: "ord[byte]", + codepoint_to_int: "ord[codepoint]", + int_to_text_byte: "char[byte]", + int_to_codepoint: "char[codepoint]", + text_replace: "replace", + text_split: "split", + text_split_whitespace: "split_whitespace", + println_int: "println[Int]", + print_int: "print[Int]", + concat: "concat[Text]", + list_push: "push", + list_length: "size[List]", + text_byte_reversed: "reversed[byte]", + text_codepoint_reversed: "reversed[codepoint]", + text_get_byte_slice: "slice[byte]", + text_get_codepoint_slice: "slice[codepoint]", + text_byte_length: "size[byte]", + text_codepoint_length: "size[codepoint]", + list_find: "find[List]", + text_to_int: "dec_to_int", + int_to_text: "int_to_dec", +}; diff --git a/src/interpreter/index.ts b/src/interpreter/index.ts new file mode 100644 index 00000000..470f4ebc --- /dev/null +++ b/src/interpreter/index.ts @@ -0,0 +1,86 @@ +import { programToSpine } from "../common/Spine"; +import { + type Node, + block, + functionCall, + isBuiltinIdent, + isOfKind, +} from "../IR"; +import { readsFromInput } from "../common/symbols"; +import { PolygolfError } from "../common/errors"; +import { compileVariant } from "../common/compile"; +import javascriptLanguage from "../languages/javascript"; +import { required } from "../common/Language"; +import { addVarDeclarations } from "../plugins/block"; + +const javascriptForInterpreting = { + ...javascriptLanguage, + phases: [ + ...javascriptLanguage.phases, + required(addVarDeclarations, { + name: "instrument", + visit(node, spine) { + const parent = spine.parent?.node; + if ( + parent !== undefined && + spine.pathFragment === "body" && + isOfKind("While", "ForEach", "ForEachKey", "ForCLike")(parent) && + (node.kind !== "Block" || + node.children[0].kind !== "FunctionCall" || + !isBuiltinIdent("instrument")(node.children[0].func)) + ) { + return block([functionCall("instrument"), node]); + } + }, + }), + ], +}; + +const outputCache = new Map(); + +export function getOutput(program: Node) { + if (!outputCache.has(program)) { + try { + outputCache.set(program, _getOutput(program)); + } catch (e) { + outputCache.set(program, e); + } + } + const res = outputCache.get(program); + if (typeof res === "string") return res; + throw res; +} + +function _getOutput(program: Node): string { + const spine = programToSpine(program); + if (spine.someNode(readsFromInput)) + throw new PolygolfError("Program reads from input."); + const jsCode = compileVariant( + program, + { level: "nogolf" }, + javascriptForInterpreting, + ); + if (typeof jsCode.result !== "string") { + throw jsCode.result; + } + let output = ""; + function print(x: string) { + output += x + "\n"; + } + function write(x: string) { + output += x; + } + const start = Date.now(); + function instrument() { + if (Date.now() - start > 500) + throw new PolygolfError("Program took too long to interpret."); + } + /* eslint-disable */ + new Function("print", "write", "instrument", jsCode.result)( + print, + write, + instrument, + ); + /* eslint-enable */ + return output; +} diff --git a/src/languages/golfscript/emit.ts b/src/languages/golfscript/emit.ts index 28124bb2..63418438 100644 --- a/src/languages/golfscript/emit.ts +++ b/src/languages/golfscript/emit.ts @@ -1,6 +1,6 @@ import { type TokenTree } from "../../common/Language"; import { EmitError, emitTextFactory } from "../../common/emit"; -import { int, integerType, type IR, isIntLiteral, isSubtype } from "../../IR"; +import { int, integerType, type IR, isInt, isSubtype } from "../../IR"; import { getType } from "../../common/getType"; const emitGolfscriptText = emitTextFactory({ @@ -35,17 +35,15 @@ export default function emitProgram(program: IR.Node): TokenTree { if (stmt.inclusive) throw new EmitError(stmt, "inclusive"); if (!isSubtype(getType(stmt.start, program), integerType(0))) throw new EmitError(stmt, "potentially negative low"); - if (stmt.variable === undefined) throw new EmitError(stmt, "indexless"); return [ emitNode(stmt.end), ",", - isIntLiteral(0n)(stmt.start) ? [] : [emitNode(stmt.start), ">"], - isIntLiteral(1n)(stmt.increment) - ? [] - : [emitNode(stmt.increment), "%"], + isInt(0n)(stmt.start) ? [] : [emitNode(stmt.start), ">"], + isInt(1n)(stmt.increment) ? [] : [emitNode(stmt.increment), "%"], "{", - ":", - emitNode(stmt.variable), + ...(stmt.variable === undefined + ? [] + : [":", emitNode(stmt.variable)]), ";", emitMultiNode(stmt.body, stmt), "}", @@ -57,11 +55,9 @@ export default function emitProgram(program: IR.Node): TokenTree { return [ emitNode(stmt.difference), ",", - isIntLiteral(1n)(stmt.increment) - ? [] - : [emitNode(stmt.increment), "%"], + isInt(1n)(stmt.increment) ? [] : [emitNode(stmt.increment), "%"], "{", - isIntLiteral()(stmt.start) && stmt.start.value < 0n + isInt()(stmt.start) && stmt.start.value < 0n ? [emitNode(int(-stmt.start.value)), "-"] : [emitNode(stmt.start), "+"], ":", @@ -105,6 +101,32 @@ export default function emitProgram(program: IR.Node): TokenTree { function emitNode(expr: IR.Node): TokenTree { switch (expr.kind) { case "Assignment": + if (expr.variable.kind === "IndexCall") + /* Implements equivalent of this Python code: + temp = (index+len(col))%len(col); coll = coll[:temp] + [expr] + coll[temp+1:]; + */ + return [ + emitNode(expr.variable.collection), + ".", + isSubtype(getType(expr.variable.index, program), integerType(0)) + ? emitNode(expr.variable.index) + : [".", ",", ".", emitNode(expr.variable.index), "+", "\\", "%"], + ".", + "@", + "<", + "[", + emitNode(expr.expr), + "]", + "+", + "@", + "@", + ")", + ">", + "+", + ":", + emitNode(expr.variable.collection), + ";", + ]; return [emitNode(expr.expr), ":", emitNode(expr.variable), ";"]; case "Identifier": return expr.name; @@ -125,18 +147,18 @@ export default function emitProgram(program: IR.Node): TokenTree { emitNode(expr.alternate), "if", ]; - case "RangeIndexCall": { - if (expr.oneIndexed) throw new EmitError(expr, "one indexed"); - + case "IndexCall": { + return [emitNode(expr.collection), emitNode(expr.index), "="]; + } + case "RangeIndexCall": return [ emitNode(expr.collection), emitNode(expr.high), "<", emitNode(expr.low), ">", - isIntLiteral(1n)(expr.step) ? [] : [emitNode(expr.step), "%"], + isInt(1n)(expr.step) ? [] : [emitNode(expr.step), "%"], ]; - } default: throw new EmitError(expr); } diff --git a/src/languages/golfscript/golfscript.test.md b/src/languages/golfscript/golfscript.test.md index 2c725dc9..711b61ee 100644 --- a/src/languages/golfscript/golfscript.test.md +++ b/src/languages/golfscript/golfscript.test.md @@ -3,20 +3,22 @@ ## Printing ```polygolf -println_int 1; -print_int 2; +println[Int] 1; +print[Int] 2; println "a"; print "b"; ``` ```golfscript nogolf -1 n 2"a"n"b" +1" +"+2"a +""b" ``` ```polygolf $x:Int <- 1; -print_int $x; -print_int $x; +print[Int] $x; +print[Int] $x; ``` ```golfscript nogolf @@ -65,41 +67,45 @@ $a >= 2; $a > 2; % Text Encoding -text_get_byte "abc" 1; -text_get_codepoint "def" 1; -text_byte_to_int "g"; -codepoint_to_int "h"; -text_get_byte_to_int "ijk" 1; -text_get_codepoint_to_int "lmn" 1; -text_byte_length "opq"; -text_codepoint_length "rst"; -int_to_text_byte 99; -int_to_codepoint 99; +at[byte] "abc" 1; +ord "g"; +ord_at[byte] "ijk" 1; +size[byte] $b; +char[byte] 99; +slice[byte] "abcdefg" 2 3; +"a" == "b"; +"a" != "b"; % Other -list_get $d 1; -list_push $d "t"; -list_length $d; +at[List] $d 1; +size[List] $d; join $d "_"; sorted $d; -concat $b "xyz"; -int_to_text 5; -text_to_int "5"; -text_split "xyz" "y"; -text_byte_reversed $b; +concat[Text] $b "xyz"; +int_to_dec 5; +int_to_bin 6; +int_to_hex 7; +int_to_bin_aligned 8 7; +int_to_hex_aligned 9 7; +dec_to_int "5"; +split "xyz" "y"; +split_whitespace "a\nb c"; +reversed[byte] $b; +reversed[List] $d; repeat $b 3; ``` ```golfscript nogolf -0:a;"xy":b;0 0=:c;["q""r""s"]:d;c c and c c or c!a~-1 a*a abs 2 a+a 2- 2 a*a 2/a 2?a 2%2 a&2 a|2 a^[2 a]$1=[2 a]$0=4 a*a 4/a 2a 2>["abc"1=]""+["def"1=]""+"g")"h")"ijk"1="lmn"1="opq","rst",[99]""+[99]""+d 1=d"t"+d,d"_"*d$b"xyz"+5`"5"~"xyz""y"/b-1%b 3* +0:A;"xy":b;0 0=:c;["q""r""s"]:d;c c and c c or c!A~-1 A*A abs 2 A+A 2- 2 A*A 2/A 2?A 2%2 A&2 A|2 A^[2 A]$1=[2 A]$0=4 A*A 4/A 2A 2>["abc"1=]""+"g")"ijk"1=b,[99]""+"abcdefg"5<2>"a""b"="a""b"=!d 1=d,d"_"*d$b"xyz"+5`6 2 base""*7 16 base{.9>7*+48+}%""+8 7 2base""+\1$,-.0>*"0"*\+9 7 16base{.9>7*+48+}%""+\1$,-.0>*"0"*\+"5"~"xyz""y"/"a +b c"{...9<\13>+*\32if}%" "/b-1%d-1%b 3* ``` ## Looping ```polygolf for $i 0 31 { - println_int ((1 + $i) + ($i * $i)); + println ((1 + $i) + ($i * $i)); }; ``` @@ -109,43 +115,57 @@ for $i 0 31 { ```polygolf for $i 5 80 5 { - println_int $i; + println[Int] $i; }; ``` ```golfscript nogolf -80,5>5%{:i;i n}% +80,5>5%{:i;i" +"+}% ``` ```polygolf for $i -5 31 { - println_int $i; + println $i; }; ``` ```golfscript nogolf -36,{5-:i;i n}% +36,{5-:i;i" +"+}% ``` ```polygolf $a:-10..10 <- -4; for $i $a ($a+6) { - println_int $i; + println $i; }; ``` ```golfscript nogolf --4:a;6,{a+:i;i n}% +-4:A;6,{A+:i;i" +"+}% +``` + +```polygolf +for 5 { + print "x"; +}; +``` + +```golfscript nogolf +5,{;"x"}% ``` ## Argv ```polygolf -println (argv_get 5); +println (at[argv] 5); ``` ```golfscript nogolf -:a;a 5=n +:a;a 5=" +"+ ``` ```polygolf @@ -155,5 +175,6 @@ for_argv $x 100 { ``` ```golfscript nogolf -:a;a{:x;x n}% +:a;a{:x;x" +"+}% ``` diff --git a/src/languages/golfscript/index.ts b/src/languages/golfscript/index.ts index 1cefd43f..4693827c 100644 --- a/src/languages/golfscript/index.ts +++ b/src/languages/golfscript/index.ts @@ -3,8 +3,6 @@ import { integerType, isSubtype, rangeIndexCall, - add1, - sub1, builtin, op, int, @@ -12,7 +10,10 @@ import { infix, list, prefix, - isIntLiteral, + isInt, + implicitConversion, + prec, + succ, } from "../../IR"; import { defaultDetokenizer, @@ -24,17 +25,34 @@ import { import emitProgram from "./emit"; import { mapOps, - mapToPrefixAndInfix, + mapUnaryAndBinary, flipBinaryOps, removeImplicitConversions, printIntToPrint, + useIndexCalls, + arraysToLists, + backwardsIndexToForwards, } from "../../plugins/ops"; -import { alias, renameIdents } from "../../plugins/idents"; -import { golfLastPrint, implicitlyConvertPrintArg } from "../../plugins/print"; +import { + alias, + defaultIdentGen, + renameIdents, + useBuiltinAliases, +} from "../../plugins/idents"; +import { + golfLastPrint, + implicitlyConvertPrintArg, + printConcatToMultiPrint, + printLnToPrint, + printToImplicitOutput, + putcToPrintChar, + splitPrint, +} from "../../plugins/print"; import { forArgvToForEach, forRangeToForDifferenceRange, forRangeToForRangeOneStep, + removeUnusedForVar, } from "../../plugins/loops"; import { addImports } from "../../plugins/imports"; import { getType } from "../../common/getType"; @@ -49,21 +67,24 @@ import { pickAnyInt, } from "../../plugins/arithmetic"; import { - useEquivalentTextOp, + usePrimaryTextOps, textGetToTextGetToIntToText, replaceToSplitAndJoin, + startsWithEndsWithToSliceEquality, } from "../../plugins/textOps"; import { inlineVariables } from "../../plugins/block"; +import { hardcode } from "../../plugins/static"; const golfscriptLanguage: Language = { name: "Golfscript", extension: "gs", emitter: emitProgram, phases: [ - required(printIntToPrint), + search(hardcode()), + required(printIntToPrint, arraysToLists, usePrimaryTextOps("byte")), + simplegolf(golfLastPrint(false)), search( flipBinaryOps, - golfLastPrint(), equalityToInequality, ...bitnotPlugins, ...powPlugins, @@ -74,21 +95,26 @@ const golfscriptLanguage: Language = { forArgvToForEach, bitShiftToMulOrDiv(false, true, true), decomposeIntLiteral(false, true, false), + splitPrint, ), required( pickAnyInt, forArgvToForEach, + putcToPrintChar, bitShiftToMulOrDiv(false, true, true), - useEquivalentTextOp(true, false), - textGetToTextGetToIntToText, + removeUnusedForVar, forRangeToForDifferenceRange( (node, spine) => !isSubtype(getType(node.start, spine.root.node), integerType(0)), ), - implicitlyConvertPrintArg, replaceToSplitAndJoin, + implicitlyConvertPrintArg, + printLnToPrint, ), simplegolf( + startsWithEndsWithToSliceEquality("byte"), + printConcatToMultiPrint, + useBuiltinAliases({ "\n": "n" }), alias({ Integer: (x) => x.value.toString(), Text: (x) => `"${x.value}"`, @@ -96,32 +122,59 @@ const golfscriptLanguage: Language = { ), required( mapOps({ - argv_get: (x) => op("list_get", op("argv"), x[0]), + "at[argv]": (x) => op["at[List]"](op.argv, x[0]), argv: builtin("a"), true: int(1), false: int(0), - print: (x) => x[0], - text_get_byte_slice: (x) => - rangeIndexCall(x[0], x[1], add1(x[2]), int(1)), - neg: (x) => op("mul", x[0], int(-1)), - max: (x) => op("list_get", op("sorted", list(x)), int(1)), - min: (x) => op("list_get", op("sorted", list(x)), int(0)), + "slice[byte]": (x) => + rangeIndexCall(x[0], x[1], op.add(x[1], x[2]), int(1)), + "slice[List]": (x) => + rangeIndexCall(x[0], x[1], op.add(x[1], x[2]), int(1)), + neg: (x) => op.mul(x[0], int(-1)), + max: (x) => op["at[List]"](op["sorted[Int]"](list(x)), int(1)), + min: (x) => op["at[List]"](op["sorted[Int]"](list(x)), int(0)), leq: (x) => - op( - "lt", - ...(isIntLiteral()(x[0]) ? [sub1(x[0]), x[1]] : [x[0], add1(x[1])]), - ), + isInt()(x[0]) ? op.lt(prec(x[0]), x[1]) : op.lt(x[0], succ(x[1])), geq: (x) => - op( - "gt", - ...(isIntLiteral()(x[0]) ? [add1(x[0]), x[1]] : [x[0], sub1(x[1])]), + isInt()(x[0]) ? op.gt(succ(x[0]), x[1]) : op.gt(x[0], prec(x[1])), + int_to_bool: (x) => implicitConversion("int_to_bool", x[0]), + bool_to_int: (x) => implicitConversion("bool_to_int", x[0]), + append: (x) => op["concat[List]"](x[0], list([x[1]])), + "contains[Text]": (x) => + implicitConversion( + "int_to_bool", + op.add(op["find[byte]"](x[0], x[1]), int(1n)), + ), + "contains[List]": (x) => + implicitConversion( + "int_to_bool", + op.add(op["find[List]"](x[0], x[1]), int(1n)), ), + int_to_bin: (x) => infix("*", infix("base", x[0], int(2n)), text("")), + + // TO-DO: less hacky implementations for these: + int_to_hex: (x) => + infix( + "+", + prefix("{.9>7*+48+}%", infix("base", x[0], int(16n))), + text(""), + ), + gcd: (x) => infix("{.}{.@@%}while;", x[0], x[1]), + split_whitespace: (x) => + op.split(prefix("{...9<\\13>+*\\32if}%", x[0]), text(" ")), + right_align: (x) => infix('1$,-.0>*" "*\\+', x[0], x[1]), + int_to_hex_aligned: (x) => + infix('16base{.9>7*+48+}%""+\\1$,-.0>*"0"*\\+', x[0], x[1]), + int_to_bin_aligned: (x) => + infix('2base""+\\1$,-.0>*"0"*\\+', x[0], x[1]), }), - mapToPrefixAndInfix({ - println: "n", + backwardsIndexToForwards(false), + textGetToTextGetToIntToText, + useIndexCalls(false), + mapUnaryAndBinary({ not: "!", bit_not: "~", mul: "*", @@ -133,48 +186,44 @@ const golfscriptLanguage: Language = { sub: "-", bit_or: "|", bit_xor: "^", - concat: "+", + "concat[Text]": "+", + "concat[List]": "+", lt: "<", - eq: "=", + "eq[Int]": "=", + "eq[Text]": "=", gt: ">", and: "and", or: "or", - text_get_byte_to_int: "=", - text_byte_length: ",", - text_byte_to_int: ")", - int_to_text: "`", - text_split: "/", + "ord_at[byte]": "=", + "size[byte]": ",", + "ord[byte]": ")", + int_to_dec: "`", + split: "/", repeat: "*", pow: "?", - text_to_int: "~", + dec_to_int: "~", abs: "abs", - list_push: "+", - list_get: "=", - list_length: ",", + "size[List]": ",", join: "*", - sorted: "$", + "sorted[Int]": "$", + "sorted[Ascii]": "$", + "find[byte]": "?", + "find[List]": "?", }), mapOps({ - neq: (x) => prefix("!", infix("=", x[0], x[1])), - text_byte_reversed: (x) => infix("%", x[0], int(-1)), - int_to_text_byte: (x) => infix("+", list(x), text("")), + "neq[Int]": (x) => prefix("!", infix("=", x[0], x[1])), + "neq[Text]": (x) => prefix("!", infix("=", x[0], x[1])), + "reversed[byte]": (x) => infix("%", x[0], int(-1)), + "reversed[List]": (x) => infix("%", x[0], int(-1)), + "char[byte]": (x) => infix("+", list(x), text("")), }), + ), + required( + printToImplicitOutput, addImports({ a: "a" }, (x) => - x.length > 0 ? assignment("a", builtin("")) : undefined, + x.length > 0 ? assignment(builtin("a"), builtin("")) : undefined, ), - renameIdents({ - // Custom Ident generator prevents `n` from being used as an ident, as it is predefined to newline and breaks printing if modified - preferred(original: string) { - const firstLetter = [...original].find((x) => /[A-Za-z]/.test(x)); - if (firstLetter === undefined) return []; - if (/n/i.test(firstLetter)) return ["N", "m", "M"]; - const lower = firstLetter.toLowerCase(); - const upper = firstLetter.toUpperCase(); - return [firstLetter, firstLetter === lower ? upper : lower]; - }, - short: "abcdefghijklmopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ".split(""), - general: (i) => `v${i}`, - }), + renameIdents(defaultIdentGen("a", "n")), removeImplicitConversions, ), ], diff --git a/src/languages/janet/emit.ts b/src/languages/janet/emit.ts new file mode 100644 index 00000000..a632a4d2 --- /dev/null +++ b/src/languages/janet/emit.ts @@ -0,0 +1,148 @@ +import { EmitError, emitIntLiteral, emitTextFactory } from "../../common/emit"; +import { isInt, type IR } from "../../IR"; +import { type TokenTree } from "../../common/Language"; + +const emitJanetText = emitTextFactory({ + '"TEXT"': { "\\": `\\\\`, "\n": `\\n`, "\r": `\\r`, '"': `\\"` }, + "`\nTEXT\n`": { "`": null }, + "``\nTEXT\n``": { "``": null }, + /* TO-DO: Introduce "`TEXT`" string literal: + Cannot be empty or begin/end with a newline + */ +}); + +export default function emitProgram(program: IR.Node): TokenTree { + function emitMultiNode(BaseNode: IR.Node, blockNeedsDo = false): TokenTree { + const children = BaseNode.kind === "Block" ? BaseNode.children : [BaseNode]; + if (BaseNode.kind === "Block" && blockNeedsDo) { + return ["(", "do", children.map((x) => emit(x)), ")"]; + } + return children.map((x) => emit(x)); + } + + /** + * Emits the expression. + * @param expr The expression to be emited. + * @returns Token tree corresponding to the expression. + */ + function emit(e: IR.Node): TokenTree { + switch (e.kind) { + case "Block": + return emitMultiNode(e); + case "While": + return ["(", "while", emit(e.condition), emitMultiNode(e.body), ")"]; + case "ForEach": + return [ + "(", + "each", + emit(e.variable), + emit(e.collection), + emitMultiNode(e.body), + ")", + ]; + case "ForRange": { + const varName = e.variable === undefined ? "_" : emit(e.variable); + return isInt(1n)(e.increment) + ? [ + "(", + "for", + varName, + emit(e.start), + emit(e.end), + emitMultiNode(e.body), + ")", + ] + : [ + "(", + "loop", + "[", + varName, + ":range", + "[", + emit(e.start), + emit(e.end), + emit(e.increment), + "]", + "]", + emitMultiNode(e.body), + ")", + ]; + } + case "If": + return [ + "(", + "if", + emit(e.condition), + emitMultiNode(e.consequent, true), + e.alternate === undefined ? [] : emitMultiNode(e.alternate, true), + ")", + ]; + case "VarDeclarationWithAssignment": { + const assignment = e.assignment; + if (assignment.kind !== "Assignment") { + throw new EmitError( + e, + `Declaration cannot contain ${assignment.kind}`, + ); + } + const assignKeyword = + assignment.expr.kind === "Identifier" && assignment.expr.builtin + ? "def" + : "var"; + return [ + "(", + assignKeyword, + emit(assignment.variable), + emit(assignment.expr), + ")", + ]; + } + case "Assignment": + return ["(", "set", emit(e.variable), emit(e.expr), ")"]; + case "Identifier": + return e.name; + case "Text": + return emitJanetText(e.value); + case "Integer": + return emitIntLiteral(e, { + 10: ["", ""], + 16: ["0x", ""], + 36: ["36r", ""], + }); + case "FunctionCall": + return ["(", emit(e.func), e.args.map((x) => emit(x)), ")"]; + case "MutatingInfix": + return [ + "(", + e.name, + "$GLUE$", + "=", + emit(e.variable), + emit(e.right), + ")", + ]; + case "RangeIndexCall": + if (!isInt(1n)(e.step)) throw new EmitError(e, "step not equal one"); + return isInt(0n)(e.low) + ? ["(", "take", emit(e.high), emit(e.collection), ")"] + : ["(", "slice", emit(e.collection), emit(e.low), emit(e.high), ")"]; + case "ConditionalOp": + return [ + "(", + "if", + emit(e.condition), + emit(e.consequent), + emit(e.alternate), + ")", + ]; + case "List": + return ["@[", e.exprs.map((x) => emit(x)), "]"]; + case "Table": + return ["@{", e.kvPairs.map((x) => [emit(x.key), emit(x.value)]), "}"]; + default: + throw new EmitError(e); + } + } + + return emitMultiNode(program); +} diff --git a/src/languages/janet/index.ts b/src/languages/janet/index.ts new file mode 100644 index 00000000..8d4c3662 --- /dev/null +++ b/src/languages/janet/index.ts @@ -0,0 +1,206 @@ +import { + type Language, + required, + defaultDetokenizer, + simplegolf, + search, +} from "../../common/Language"; +import emitProgram from "./emit"; +import { + addIncAndDec, + arraysToLists, + flipBinaryOps, + mapOps, + mapTo, + removeImplicitConversions, + mapUnaryAndBinary, +} from "../../plugins/ops"; +import { addVarDeclarations } from "../../plugins/block"; +import { + succ, + builtin, + conditional, + functionCall as func, + int, + list, + op, + rangeIndexCall, + text, +} from "../../IR"; +import { + golfLastPrint, + golfLastPrintInt, + putcToPrintChar, +} from "../../plugins/print"; +import { usePrimaryTextOps } from "../../plugins/textOps"; +import { golfStringListLiteral, listOpsToTextOps } from "../../plugins/static"; +import { + applyDeMorgans, + bitnotPlugins, + equalityToInequality, + lowBitsPlugins, + pickAnyInt, + truncatingOpsPlugins, +} from "../../plugins/arithmetic"; +import { forArgvToForEach } from "../../plugins/loops"; +import { alias, renameIdents } from "../../plugins/idents"; +import { assertInt64 } from "../../plugins/types"; +import { implicitlyConvertConcatArg } from "./plugins"; + +const janetLanguage: Language = { + name: "Janet", + extension: "janet", + emitter: emitProgram, + phases: [ + required(arraysToLists, putcToPrintChar, usePrimaryTextOps("byte")), + simplegolf(golfLastPrint(false), golfLastPrintInt(true)), + search( + flipBinaryOps, + golfStringListLiteral(false), + listOpsToTextOps(), + equalityToInequality, + ...bitnotPlugins, + ...lowBitsPlugins, + applyDeMorgans, + ), + + required( + pickAnyInt, + forArgvToForEach, + ...truncatingOpsPlugins, + mapOps({ + right_align: (x) => + func( + "string/format", + op["concat[Text]"](text("%"), op.int_to_dec(x[1]), text("s")), + x[0], + ), + int_to_hex_aligned: (x) => + func( + "string/format", + op["concat[Text]"](text("%0"), op.int_to_dec(x[1]), text("X")), + x[0], + ), + }), + ), + simplegolf(implicitlyConvertConcatArg), + required( + mapOps({ + argv: func("slice", func("dyn", builtin(":args")), int(1n)), + + append: (x) => op["concat[List]"](x[0], list([x[1]])), + + "at[argv]": (x) => + op["at[List]"](func("dyn", builtin(":args")), succ(x[0])), + "at[byte]": (x) => op["slice[byte]"](x[0], x[1], int(1n)), + "contains[Text]": (x) => func("int?", op["find[byte]"](x[0], x[1])), + "contains[Table]": (x) => + op.not(func("nil?", op["at[Table]"](x[0], x[1]))), + }), + mapOps({ + true: builtin("true"), + false: builtin("false"), + + bool_to_int: (x) => conditional(x[0], int(1n), int(0n)), + int_to_bool: (x) => op["neq[Int]"](x[0], int(0n)), + int_to_hex: (x) => func("string/format", text("%X"), x[0]), + split: (x) => func("string/split", x[1], x[0]), + + "char[byte]": (x) => func("string/format", text("%c"), x[0]), + "concat[List]": (x) => func("array/concat", list([]), ...x), + "find[byte]": (x) => func("string/find", x[1], x[0]), + "ord[byte]": (x) => op["ord_at[byte]"](x[0], int(0n)), + "slice[byte]": (x) => + rangeIndexCall(x[0], x[1], op.add(x[1], x[2]), int(1n)), + "slice[List]": (x) => + rangeIndexCall(x[0], x[1], op.add(x[1], x[2]), int(1n)), + }), + mapTo(func)({ + replace: "string/replace-all", + + "concat[Text]": "string", + "set_at[List]": "put", + "set_at[Table]": "put", + }), + mapUnaryAndBinary( + { + abs: "math/abs", + add: "+", + and: "and", + bit_and: "band", + bit_not: "bnot", + bit_or: "bor", + bit_shift_left: "blshift", + bit_shift_right: "brshift", + bit_xor: "bxor", + dec_to_int: "eval-string", + gcd: "math/gcd", + geq: ">=", + gt: ">", + int_to_dec: "string", + join: "string/join", + leq: "<=", + lt: "<", + max: "max", + min: "min", + mul: "*", + neg: "-", + not: "not", + or: "or", + pow: "math/pow", + push: "array/push", + rem: "%", + repeat: "string/repeat", + sub: "-", + trunc_div: "div", + + "at[List]": "", + "at[Table]": "", + "eq[Int]": "=", + "eq[Text]": "=", + "size[byte]": "length", + "size[List]": "length", + "size[Table]": "length", + "neq[Int]": "not=", + "neq[Text]": "not=", + "ord_at[byte]": "", + "println[Int]": "pp", + "println[Text]": "print", + "print[Int]": "prin", + "print[Text]": "prin", + "reversed[byte]": "reverse", + "reversed[List]": "reverse", + "sorted[Ascii]": "sorted", + "sorted[Int]": "sorted", + }, + ["+", "-", "*", "%"], + func, + func, + ), + ), + simplegolf( + addIncAndDec((x) => func(x.name.repeat(2), x.variable)), + alias({ + Identifier: (n, s) => + n.builtin && s.pathFragment !== "ident" ? n.name : undefined, + Integer: (x) => x.value.toString(), + Text: (x) => `"${x.value}"`, + }), + ), + required( + renameIdents(), + addVarDeclarations, + removeImplicitConversions, + assertInt64, + ), + ], + detokenizer: defaultDetokenizer( + (a, b) => + a !== "" && + b !== "" && + /[^(){}[\]`'"]/.test(a[a.length - 1]) && + /[^(){}[\]`'"]/.test(b[0]), + ), +}; + +export default janetLanguage; diff --git a/src/languages/janet/janet.test.md b/src/languages/janet/janet.test.md new file mode 100644 index 00000000..ffefcd63 --- /dev/null +++ b/src/languages/janet/janet.test.md @@ -0,0 +1,196 @@ +# Janet + +## Printing + +```polygolf +$a <- 1; +$b <- "x"; +println $a; +print $a; +println $b; +print $b; +``` + +```janet nogolf +(var a 1)(var b"x")(pp a)(prin a)(print b)(prin b) +``` + +## Ops emit + +```polygolf +$a:-100..100 <- 0; +$b:Text <- "xy"; +$c <- (0==0); +$d <- (list "q" "r" "s"); + +% Boolean +and $c $c; +or $c $c; +not $c; + +% Unary Arithmetic +~ $a; +- $a; +abs $a; + +% Binary Arithmetic +$a + 2; +$a - 2; +$a * 2; +$a div 2; +$a ^ 2; +$a mod 2; +$a & 2; +$a | 2; +$a ~ 2; +max $a 2; +min $a 2; + +% Comparison +$a << 2; +$a >> 2; +$a < 2; +$a <= 2; +$a == 2; +$a != 2; +$a >= 2; +$a > 2; + +% Text Encoding +at[byte] "abc" 1; +ord "g"; +ord_at[byte] "ijk" 1; +size[byte] $b; +char[byte] 99; +slice[byte] "abcdefg" 2 3; +slice[byte] "abcdefg" 0 3; +"a" == "b"; +"a" != "b"; + +% Other +at[Table] (table (1 => 2)) 1; +at[List] $d 1; +size[List] $d; +join $d "_"; +sorted $d; +concat[Text] $b "xyz"; +int_to_dec 5; +int_to_hex 7; +dec_to_int "5"; +split "xyz" "y"; +reversed[byte] $b; +reversed[List] $d; +repeat $b 3; +right_align "he" 7; +int_to_hex_aligned 31 7; +``` + +```janet nogolf +(var a 0)(var b"xy")(var c(= 0 0))(var d @["q""r""s"])(and c c)(or c c)(not c)(bnot a)(- a)(math/abs a)(+ 2 a)(- a 2)(* 2 a)(div a 2)(math/pow a 2)(% a 2)(band 2 a)(bor 2 a)(bxor 2 a)(max 2 a)(min 2 a)(blshift a 2)(brshift a 2)(< a 2)(<= a 2)(= a 2)(not= a 2)(>= a 2)(> a 2)(slice"abc"1 2)("g"0)("ijk"1)(length b)(string/format"%c"99)(slice"abcdefg"2 5)(take 3"abcdefg")(="a""b")(not="a""b")(@{1 2}1)(d 1)(length d)(string/join d"_")(sorted d)(string b"xyz")(string 5)(string/format"%X"7)(eval-string"5")(string/split"y""xyz")(reverse b)(reverse d)(string/repeat b 3)(string/format(string"%"(string 7)"s")"he")(string/format(string"%0"(string 7)"X")31) +``` + +## Looping + +```polygolf +for $i 0 31 { + println ((1 + $i) + ($i * $i)); +}; +``` + +```janet nogolf +(for i 0 31(pp(+(+ 1 i)(* i i)))) +``` + +```polygolf +$a:-10..10 <- -4; +for $i $a ($a+6) { + println $i; +}; +``` + +```janet nogolf +(var a -4)(for i a(+ 6 a)(pp i)) +``` + +```polygolf +for 5 { + print "x"; +}; +``` + +```janet nogolf +(for _ 0 5(prin"x")) +``` + +```polygolf +while (> 1 0) { + println 5; + $x <- 1; +}; +``` + +```janet nogolf +(while(> 1 0)(pp 5)(var x 1)) +``` + +## Argv + +```polygolf +println (at[argv] 5); +``` + +```janet nogolf +(print((dyn :args)6)) +``` + +```polygolf +for_argv $x 100 { + println $x; +}; +``` + +```janet nogolf +(each x(slice(dyn :args)1)(print x)) +``` + +## Block in if statement + +```polygolf +if (> 1 0) { + $x <- 1; + print $x; +} { + $y <- 2; + println $y; +}; +``` + +```janet nogolf +(if(> 1 0)(do(var x 1)(prin x))(do(var y 2)(pp y))) +``` + +## Mutating ops + +```polygolf +$a:-10..10 <- 3; +$a <- (mod $a 4):-10..10; +$a <- (* $a 5):-10..10; +$a <- (+ $a 3):-10..10; +$a <- (- $a 3):-10..10; +$a <- (+ $a 1):-10..10; +$a <- (- $a 1):-10..10; +``` + +```janet addIncAndDec +(var a 3)(%= a 4)(*= a 5)(+= a 3)(-= a 3)(++ a)(-- a) +``` + +## implicitlyConvertConcatArg + +```polygolf +(.. "he" (int_to_dec 11) "o"); +``` + +```janet implicitlyConvertConcatArg +(string"he"11"o") +``` diff --git a/src/languages/janet/plugins.ts b/src/languages/janet/plugins.ts new file mode 100644 index 00000000..69e1324d --- /dev/null +++ b/src/languages/janet/plugins.ts @@ -0,0 +1,12 @@ +import { type Spine } from "../../common/Spine"; +import { implicitConversion, isOp, type Node } from "../../IR"; + +export function implicitlyConvertConcatArg(node: Node, spine: Spine) { + if ( + isOp("int_to_dec")(node) && + !spine.isRoot && + isOp("concat[Text]")(spine.parent!.node) + ) { + return implicitConversion(node.op, node.args[0]); + } +} diff --git a/src/languages/javascript/emit.ts b/src/languages/javascript/emit.ts index 5d43548e..2a91ac2b 100644 --- a/src/languages/javascript/emit.ts +++ b/src/languages/javascript/emit.ts @@ -115,10 +115,20 @@ export default function emitProgram( const prec = precedence(expr); function emitNoParens(e: IR.Node): TokenTree { switch (e.kind) { + case "VarDeclarationWithAssignment": + return ["let", emit(e.assignment)]; case "Block": return expr === program ? joinNodes("\n", e.children) - : e.children.some(isOfKind("If", "While", "ForEach", "ForCLike")) + : e.children.some( + isOfKind( + "If", + "While", + "ForEach", + "ForCLike", + "VarDeclarationWithAssignment", + ), + ) ? ["{", joinNodes("\n", e.children), "}"] : joinNodes(",", e.children); case "Function": @@ -158,11 +168,11 @@ export default function emitProgram( ]; case "ConditionalOp": return [ - emit(e.condition), + emit(e.condition, prec + 1), "?", emit(e.consequent), ":", - emit(e.alternate), + emit(e.alternate, prec), ]; case "While": return [`while`, "(", emit(e.condition), ")", emit(e.body)]; @@ -180,7 +190,6 @@ export default function emitProgram( case "MutatingInfix": return [emit(e.variable), e.name + "=", emit(e.right)]; case "IndexCall": - if (e.oneIndexed) throw new EmitError(expr, "one indexed"); return [emit(e.collection, Infinity), "[", emit(e.index), "]"]; case "PropertyCall": return [emit(e.object, prec), ".", emit(e.ident)]; diff --git a/src/languages/javascript/index.ts b/src/languages/javascript/index.ts index fca29ce3..336c4471 100644 --- a/src/languages/javascript/index.ts +++ b/src/languages/javascript/index.ts @@ -12,23 +12,27 @@ import { isText, text, implicitConversion, + list, + prefix, } from "../../IR"; import { type Language, required, search, simplegolf, + flattenTree, + defaultWhitespaceInsertLogic, } from "../../common/Language"; import emitProgram from "./emit"; import { mapOps, - mapToPrefixAndInfix, + mapUnaryAndBinary, useIndexCalls, removeImplicitConversions, printIntToPrint, mapTo, - addPostfixIncAndDec, + addIncAndDec, methodsAsFunctions, } from "../../plugins/ops"; import { alias, renameIdents } from "../../plugins/idents"; @@ -37,8 +41,12 @@ import { forRangeToForCLike, forRangeToForEach, } from "../../plugins/loops"; -import { golfStringListLiteral } from "../../plugins/static"; -import { golfLastPrint, implicitlyConvertPrintArg } from "../../plugins/print"; +import { golfStringListLiteral, hardcode } from "../../plugins/static"; +import { + golfLastPrint, + implicitlyConvertPrintArg, + putcToPrintChar, +} from "../../plugins/print"; import { useDecimalConstantPackedPrinter, useLowDecimalListPackedPrinter, @@ -46,8 +54,8 @@ import { import { replaceToSplitAndJoin, textGetToIntToTextGet, + textToIntToFirstIndexTextGetToInt, textToIntToTextGetToInt, - useEquivalentTextOp, } from "../../plugins/textOps"; import { addOneToManyAssignments, inlineVariables } from "../../plugins/block"; import { @@ -57,6 +65,7 @@ import { equalityToInequality, lowBitsPlugins, pickAnyInt, + truncatingOpsPlugins, useIntegerTruthiness, } from "../../plugins/arithmetic"; import { tableToListLookup } from "../../plugins/tables"; @@ -68,11 +77,12 @@ const javascriptLanguage: Language = { extension: "js", emitter: emitProgram, phases: [ + search(hardcode()), required(printIntToPrint), + simplegolf(golfLastPrint()), search( golfStringListLiteral(), - forRangeToForEach("array_get", "list_get", "text_get_codepoint"), - golfLastPrint(), + forRangeToForEach("at[Array]", "at[List]", "at[codepoint]"), equalityToInequality, useDecimalConstantPackedPrinter, useLowDecimalListPackedPrinter, @@ -85,9 +95,9 @@ const javascriptLanguage: Language = { inlineVariables, forArgvToForEach, replaceToSplitAndJoin, - useEquivalentTextOp(false, true), useIndexCalls(), decomposeIntLiteral(), + forRangeToForEachKey, ), required( pickAnyInt, @@ -105,77 +115,122 @@ const javascriptLanguage: Language = { bit_shift_right: "bigint", lt: "int", leq: "int", - eq: "int", - neq: "int", + "eq[Int]": "int", + "neq[Int]": "int", geq: "int", gt: "int", + int_to_dec: "bigint", }), mapVarsThatNeedBigint("int53", (x) => func("BigInt", x)), forArgvToForEach, + putcToPrintChar, ), - simplegolf(forRangeToForEachKey), required( forRangeToForCLike, - useEquivalentTextOp(false, true), mapOps({ - text_to_int: (x) => - op("add", int(0n), implicitConversion("text_to_int", x[0])), argv: builtin("arguments"), - argv_get: (x) => - op( - "list_get", + "at[argv]": (x) => + op["at[List]"]( { ...builtin("arguments"), type: listType(textType()) }, x[0], ), }), useIndexCalls(), + ...truncatingOpsPlugins, textGetToIntToTextGet, implicitlyConvertPrintArg, + textToIntToFirstIndexTextGetToInt, mapOps({ true: builtin("true"), false: builtin("false"), - text_get_codepoint: (x) => indexCall(x[0], x[1]), + "at[Ascii]": (x) => indexCall(x[0], x[1]), + "slice[List]": (x) => method(x[0], "slice", x[1], op.add(x[1], x[2])), + "slice[Ascii]": (x) => method(x[0], "slice", x[1], op.add(x[1], x[2])), + "char[Ascii]": (x) => func("String.fromCharCode", x), + "char[byte]": (x) => func("String.fromCharCode", x), + "sorted[Ascii]": (x) => + method( + x[0].kind === "List" ? x[0] : list([prefix("...", x[0])]), + "sort", + ), div: (x, s) => s.node.targetType !== "bigint" ? func("Math.floor", infix("/", x[0], x[1])) : undefined, - int_to_bin: (x) => method(x[0], "toString", int(2)), - int_to_hex: (x) => method(x[0], "toString", int(16)), - list_length: (x) => propertyCall(x[0], "length"), + trunc_div: (x, s) => + s.node.targetType !== "bigint" + ? func("Math.floor", infix("/", x[0], x[1])) + : undefined, + int_to_bin: (x) => method(x[0], "toString", int(2n)), + int_to_bin_aligned: (x) => + method(method(x[0], "toString", int(2n)), "padStart", x[1], int(0n)), + int_to_hex: (x) => method(x[0], "toString", int(16n)), + int_to_hex_aligned: (x) => + method(method(x[0], "toString", int(16n)), "padStart", x[1], int(0n)), + "size[List]": (x) => propertyCall(x[0], "length"), + "size[Ascii]": (x) => propertyCall(x[0], "length"), + "size[Table]": (x) => propertyCall(func("Object.keys", x[0]), "length"), + right_align: (x) => method(x[0], "padStart", x[1]), join: (x) => method(x[0], "join", ...(isText(",")(x[1]) ? [] : [x[1]])), - int_to_text: (x) => - op("concat", text(""), implicitConversion("int_to_text", x[0])), - text_to_int: (x) => - op("mul", int(1n), implicitConversion("text_to_int", x[0])), + int_to_dec: (x) => + op["concat[Text]"](text(""), implicitConversion("int_to_dec", x[0])), + dec_to_int: (x) => + op.bit_not(op.bit_not(implicitConversion("dec_to_int", x[0]))), + "reversed[List]": (x) => method(x[0], "reverse"), + "reversed[Ascii]": (x) => + method( + method(list([prefix("...", x[0])]), "reverse"), + "join", + text(""), + ), + "reversed[codepoint]": (x) => + method( + method(list([prefix("...", x[0])]), "reverse"), + "join", + text(""), + ), + append: (x) => op["concat[List]"](x[0], list([x[1]])), + bool_to_int: (x) => implicitConversion("bool_to_int", x[0]), + int_to_bool: (x) => implicitConversion("int_to_bool", x[0]), + "contains[Table]": (x) => infix("in", x[1], x[0]), }), mapTo((name: string, [obj, ...args]) => method(obj, name, ...args))({ - list_contains: "includes", - list_push: "push", - list_find: "indexOf", - text_split: "split", - text_replace: "replaceAll", + "ord_at[Ascii]": "charCodeAt", + "contains[List]": "includes", + "contains[Array]": "includes", + "contains[Text]": "includes", + push: "push", + include: "add", + "find[List]": "indexOf", + "find[Ascii]": "indexOf", + split: "split", + replace: "replaceAll", repeat: "repeat", - text_contains: "includes", + starts_with: "startsWith", + ends_with: "endsWith", }), mapTo(func)({ abs: "abs", max: "Math.max", min: "Math.min", - println: "print", - print: "write", + "println[Text]": "print", + "print[Text]": "write", }), - mapToPrefixAndInfix( + mapUnaryAndBinary( { pow: "**", neg: "-", bit_not: "~", mul: "*", div: "/", + trunc_div: "/", mod: "%", + rem: "%", add: "+", - concat: "+", + "concat[Text]": "+", + "concat[List]": "+", sub: "-", bit_shift_left: "<<", bit_shift_right: ">>", @@ -184,8 +239,10 @@ const javascriptLanguage: Language = { bit_or: "|", lt: "<", leq: "<=", - eq: "==", - neq: "!=", + "eq[Int]": "==", + "eq[Text]": "==", + "neq[Int]": "!=", + "neq[Text]": "!=", geq: ">=", gt: ">", not: "!", @@ -194,10 +251,9 @@ const javascriptLanguage: Language = { }, ["**", "*", "/", "%", "+", "-", "<<", ">>", "&", "^", "|", "&&", "||"], ), - addPostfixIncAndDec, methodsAsFunctions, - addOneToManyAssignments(), ), + simplegolf(addIncAndDec(), addOneToManyAssignments()), search(propertyCallToIndexCall), simplegolf( alias({ @@ -212,6 +268,23 @@ const javascriptLanguage: Language = { ), required(renameIdents(), removeImplicitConversions), ], + detokenizer(tree) { + let result = ""; + flattenTree(tree).forEach((token, i, tokens) => { + if (i === tokens.length - 1) result += token; + else { + const nextToken = tokens[i + 1]; + if (token === "\n" && "([`+-/".includes(nextToken[0])) { + token = ";"; + } + result += token; + if (defaultWhitespaceInsertLogic(token, nextToken)) { + result += " "; + } + } + }); + return result; + }, }; export default javascriptLanguage; diff --git a/src/languages/javascript/javascript.test.md b/src/languages/javascript/javascript.test.md index f967d340..7b0a6e0c 100644 --- a/src/languages/javascript/javascript.test.md +++ b/src/languages/javascript/javascript.test.md @@ -91,17 +91,14 @@ t+"bc" "x".repeat(10) t.includes`sub` t.replaceAll(" ","-") -t.split` ` -[t,"www"].join() -[t,"www"].join` ` +t.split` `;[t,"www"].join();[t,"www"].join` ` abs(m) -~m --m +~m;-m !b ""+m m.toString(2) m.toString(16) -0+t +~~t ``` ```polygolf @@ -209,6 +206,7 @@ $x: Int <- 5; $y: 0..100 <- 2; println_int ((3 + $x) * $y); $t <- (4 + $y); +$z <- ($x - 1); ``` ```js nogolf @@ -216,6 +214,20 @@ x=5n y=2 print((3n+x)*BigInt(y)) t=4+y +z=x-1n +``` + +```polygolf +for $a 1 100 { + println ((pow 2 $a) ~ 3); + println ((pow 2 $a) | 3); + println ((pow 2 $a) & 3); + println ((pow 2 $a) mod 3); +}; +``` + +```js nogolf +for(a=1;a<100;a+=1)print(3n^2n**BigInt(a)),print(3n|2n**BigInt(a)),print(3n&2n**BigInt(a)),print(2n**BigInt(a)%3n) ``` ## Fixed length for loop @@ -229,3 +241,23 @@ for $i 25 { ```js for(i in{}+1e9)print(i) ``` + +```polygolf +for $a 0 16 { + println (+ $a 20); +}; +``` + +```js +for(a in{}+1)print(20+~~a) +``` + +## Conditional associativity + +```polygolf +print_int (conditional (conditional (1 > 0) (1 < 0) (1 > 0)) 1 2); +``` + +```js nogolf +write((1>0?1<0:1>0)?1:2) +``` diff --git a/src/languages/javascript/plugins.ts b/src/languages/javascript/plugins.ts index 65a28868..1d1b8115 100644 --- a/src/languages/javascript/plugins.ts +++ b/src/languages/javascript/plugins.ts @@ -1,29 +1,40 @@ -import { builtin, forEachKey, indexCall, isIntLiteral, text } from "../../IR"; -import type { Plugin } from "../../common/Language"; +import { + type Node, + builtin, + forEachKey, + indexCall, + isInt, + text, + id, + block, + assignment, + op, + annotate, + tableType, + textType, + integerType, +} from "../../IR"; -export const propertyCallToIndexCall: Plugin = { - name: "propertyCallToIndexCall", - visit(node) { - if (node.kind === "PropertyCall") { - return indexCall(node.object, text(node.ident.name)); - } - }, -}; +export function propertyCallToIndexCall(node: Node) { + if (node.kind === "PropertyCall") { + return indexCall(node.object, text(node.ident.name)); + } +} -export const forRangeToForEachKey: Plugin = { - name: "forRangeToForEachKey", - visit(node) { - if ( - node.kind === "ForRange" && - node.variable !== undefined && - isIntLiteral(0n)(node.start) && - isIntLiteral()(node.end) && - 2 <= node.end.value && - node.end.value <= 37 - ) { - const end = Number(node.end.value); - return forEachKey( - node.variable, +export function forRangeToForEachKey(node: Node) { + if ( + node.kind === "ForRange" && + node.variable !== undefined && + isInt(0n)(node.start) && + isInt()(node.end) && + 2 <= node.end.value && + node.end.value <= 37 + ) { + const end = Number(node.end.value); + const loopVar = id(node.variable.name + id().name); + return forEachKey( + loopVar, + annotate( builtin( [ "'??'", @@ -65,8 +76,9 @@ export const forRangeToForEachKey: Plugin = { "{}+Map", ][end - 2], ), - node.body, - ); - } - }, -}; + tableType(textType(integerType(1, 2), true), textType()), + ), + block([assignment(node.variable, op.dec_to_int(loopVar)), node.body]), + ); + } +} diff --git a/src/languages/languages.ts b/src/languages/languages.ts index 4d702e88..2790cd9a 100644 --- a/src/languages/languages.ts +++ b/src/languages/languages.ts @@ -7,16 +7,20 @@ import swiftLanguage from "./swift"; import golfscriptLanguage from "./golfscript"; import javascriptLanguage from "./javascript"; import texLanguage from "./tex"; +import janetLanguage from "./janet"; +import textLanguage from "./text"; const languages = [ + polygolfLanguage, golfscriptLanguage, luaLanguage, nimLanguage, pythonLanguage, swiftLanguage, - polygolfLanguage, javascriptLanguage, texLanguage, + janetLanguage, + textLanguage, ]; export default languages; diff --git a/src/languages/lua/emit.ts b/src/languages/lua/emit.ts index 9092bcb1..51e11e71 100644 --- a/src/languages/lua/emit.ts +++ b/src/languages/lua/emit.ts @@ -5,7 +5,7 @@ import { emitTextFactory, joinTrees, } from "../../common/emit"; -import { type IR, isIntLiteral } from "../../IR"; +import { type IR, isInt } from "../../IR"; import { type TokenTree } from "@/common/Language"; const emitLuaText = emitTextFactory( @@ -118,7 +118,7 @@ export default function emitProgram( emit(e.start), ",", emit(e.end), - isIntLiteral(1n)(e.increment) ? [] : [",", emit(e.increment)], + isInt(1n)(e.increment) ? [] : [",", emit(e.increment)], "do", emit(e.body), "end", @@ -169,11 +169,14 @@ export default function emitProgram( case "Prefix": return [e.name, emit(e.arg, prec)]; case "IndexCall": - if (!e.oneIndexed) throw new EmitError(e, "zero indexed"); return [emit(e.collection, Infinity), "[", emit(e.index), "]"]; case "List": case "Array": return ["{", joinNodes(",", e.exprs), "}"]; + case "Table": + return ["{", joinNodes(",", e.kvPairs), "}"]; + case "KeyValue": + return [emit(e.key), "=", emit(e.value)]; default: throw new EmitError(e); diff --git a/src/languages/lua/index.ts b/src/languages/lua/index.ts index 6d70dd2f..198826f1 100644 --- a/src/languages/lua/index.ts +++ b/src/languages/lua/index.ts @@ -6,7 +6,7 @@ import { op, text, textType, - add1, + succ, isText, builtin, } from "../../IR"; @@ -26,22 +26,29 @@ import { import emitProgram from "./emit"; import { mapOps, - mapToPrefixAndInfix, + mapUnaryAndBinary, useIndexCalls, flipBinaryOps, removeImplicitConversions, printIntToPrint, mapTo, + backwardsIndexToForwards, } from "../../plugins/ops"; import { alias, renameIdents } from "../../plugins/idents"; import { tempVarToMultipleAssignment, inlineVariables, } from "../../plugins/block"; -import { golfLastPrint, implicitlyConvertPrintArg } from "../../plugins/print"; import { + golfLastPrint, + implicitlyConvertPrintArg, + putcToPrintChar, + mergePrint, +} from "../../plugins/print"; +import { + startsWithEndsWithToSliceEquality, textToIntToFirstIndexTextGetToInt, - useEquivalentTextOp, + usePrimaryTextOps, } from "../../plugins/textOps"; import { assertInt64 } from "../../plugins/types"; import { @@ -53,19 +60,23 @@ import { pickAnyInt, useIntegerTruthiness, } from "../../plugins/arithmetic"; -import { listOpsToTextOps } from "../../plugins/static"; +import { hardcode, listOpsToTextOps } from "../../plugins/static"; import { base10DecompositionToFloatLiteralAsBuiltin } from "./plugins"; +import { getType } from "../../common/getType"; +import { conditionalOpToAndOr } from "../../plugins/conditions"; const luaLanguage: Language = { name: "Lua", extension: "lua", emitter: emitProgram, phases: [ - required(printIntToPrint), + search(hardcode()), + required(printIntToPrint, putcToPrintChar, usePrimaryTextOps("byte")), + simplegolf(golfLastPrint()), search( + mergePrint, flipBinaryOps, - golfLastPrint(), - listOpsToTextOps("text_byte_find", "text_get_byte"), + listOpsToTextOps("find[byte]", "at[byte]"), tempVarToMultipleAssignment, equalityToInequality, shiftRangeOneUp, @@ -78,17 +89,17 @@ const luaLanguage: Language = { forArgvToForRange(), forRangeToForRangeInclusive(), implicitlyConvertPrintArg, - useEquivalentTextOp(true, false), textToIntToFirstIndexTextGetToInt, mapOps({ - text_to_int: (x) => - op("add", int(0n), implicitConversion("text_to_int", x[0])), - argv_get: (x) => - op("list_get", { ...builtin("arg"), type: textType() }, x[0]), + dec_to_int: (x) => + op.add(int(0n), implicitConversion("dec_to_int", x[0])), + "at[argv]": (x) => + op["at[List]"]({ ...builtin("arg"), type: textType() }, x[0]), - text_get_byte_to_int: (x) => method(x[0], "byte", add1(x[1])), - text_get_byte: (x) => method(x[0], "sub", add1(x[1]), add1(x[1])), - text_get_byte_slice: (x) => method(x[0], "sub", x[1], add1(x[2])), + "ord_at[byte]": (x) => method(x[0], "byte", succ(x[1])), + "at[byte]": (x) => method(x[0], "sub", succ(x[1]), succ(x[1])), + "slice[byte]": (x) => + method(x[0], "sub", succ(x[1]), op.add(x[1], x[2])), }), useIndexCalls(true), decomposeIntLiteral(true, true, true), @@ -98,30 +109,38 @@ const luaLanguage: Language = { forArgvToForRange(), forRangeToForRangeInclusive(), implicitlyConvertPrintArg, - useEquivalentTextOp(true, false), textToIntToFirstIndexTextGetToInt, + startsWithEndsWithToSliceEquality("byte"), mapOps({ - text_to_int: (x) => - op("mul", int(1n), implicitConversion("text_to_int", x[0])), - argv_get: (x) => - op("list_get", { ...builtin("arg"), type: textType() }, x[0]), - text_get_byte_to_int: (x) => method(x[0], "byte", add1(x[1])), - text_get_byte: (x) => method(x[0], "sub", add1(x[1]), add1(x[1])), - text_get_byte_slice: (x) => method(x[0], "sub", x[1], add1(x[2])), + dec_to_int: (x) => + op.mul(int(1n), implicitConversion("dec_to_int", x[0])), + "at[argv]": (x) => + op["at[List]"]({ ...builtin("arg"), type: textType() }, x[0]), + "ord_at[byte]": (x) => method(x[0], "byte", succ(x[1])), + "ord_at_back[byte]": (x) => method(x[0], "byte", x[1]), + "at[byte]": (x) => method(x[0], "sub", succ(x[1]), succ(x[1])), + "at_back[byte]": (x) => method(x[0], "sub", x[1], x[1]), + "slice[byte]": (x) => + method(x[0], "sub", succ(x[1]), op.add(x[1], x[2])), }), + conditionalOpToAndOr( + (n, s) => !["boolean", "void"].includes(getType(n, s).kind), + "List", + ), + backwardsIndexToForwards(), useIndexCalls(true), mapOps({ - int_to_text: (x) => - op("concat", text(""), implicitConversion("int_to_text", x[0])), + int_to_dec: (x) => + op["concat[Text]"](text(""), implicitConversion("int_to_dec", x[0])), join: (x) => func("table.concat", isText("")(x[1]) ? [x[0]] : x), - text_byte_length: (x) => method(x[0], "len"), + "size[byte]": (x) => method(x[0], "len"), true: builtin("true"), false: builtin("false"), repeat: (x) => method(x[0], "rep", x[1]), argv: builtin("arg"), - int_to_text_byte: (x) => func("string.char", x), + "char[byte]": (x) => func("string.char", x), - text_replace: ([a, b, c]) => + replace: ([a, b, c]) => method( a, "gsub", @@ -139,9 +158,10 @@ const luaLanguage: Language = { ), }), mapTo(func)({ - read_line: "io.read", - print: "io.write", - println: "print", + "read[line]": "io.read", + "print[Text]": "io.write", + "println[Text]": "print", + "reversed[byte]": "string.reverse", min: "math.min", max: "math.max", abs: "math.abs", @@ -150,18 +170,20 @@ const luaLanguage: Language = { simplegolf(base10DecompositionToFloatLiteralAsBuiltin), required( - mapToPrefixAndInfix({ + mapUnaryAndBinary({ pow: "^", not: "not", neg: "-", - list_length: "#", + "size[List]": "#", + "size[Table]": "#", + "size[byte]": "#", bit_not: "~", mul: "*", div: "//", mod: "%", add: "+", sub: "-", - concat: "..", + "concat[Text]": "..", bit_shift_left: "<<", bit_shift_right: ">>", bit_and: "&", @@ -169,8 +191,10 @@ const luaLanguage: Language = { bit_or: "|", lt: "<", leq: "<=", - eq: "==", - neq: "~=", + "eq[Int]": "==", + "eq[Text]": "==", + "neq[Int]": "~=", + "neq[Text]": "~=", geq: ">=", gt: ">", and: "and", @@ -179,6 +203,11 @@ const luaLanguage: Language = { ), simplegolf( alias({ + Identifier: (n, s) => + n.builtin && + (s.parent?.node.kind !== "MethodCall" || s.pathFragment !== "ident") + ? n.name + : undefined, Integer: (x) => x.value.toString(), Text: (x) => `"${x.value}"`, }), diff --git a/src/languages/lua/lua.test.md b/src/languages/lua/lua.test.md index 455e5f31..4b37895c 100644 --- a/src/languages/lua/lua.test.md +++ b/src/languages/lua/lua.test.md @@ -27,6 +27,8 @@ print("y") ```polygolf $a:-100..100 <- 0; $b:Text <- "xy"; +$c <- true; +$L <- (list "a" "b"); ~ $a; - $a; $a + 2; @@ -45,25 +47,33 @@ $a <= 2; $a == 2; $a >= 2; $a > 2; -array_get (array "xy" "abc") 1; -text_get_byte "abc" 1; +array_get (array "xy" $b) 1; +$L @ -1; +$L @ -2; +text_get_byte $b 1; +$b:Ascii @ -2; +text_get_byte_slice "abcdefg" 2 3; text_byte_to_int "a"; -text_get_byte_to_int "abc" 1; +text_get_byte_to_int $b 1; int_to_text_byte 99; concat $b "xyz"; -text_byte_length "abc"; +text_byte_length $b; int_to_text 5; text_to_int "5"; text_replace $b "a" "A"; text_replace $b "(" "*"; text_replace $b $b:(Text 1..oo) $b; -join (list "xy" "abc") "/"; +join (list "xy" $b) "/"; join (list "12" "345") ""; +conditional ($a == 2) $a 3; +conditional ($a == 2) $c false; ``` ```lua nogolf a=0 b="xy" +c=true +L={"a","b"} ~a -a 2+a @@ -82,32 +92,42 @@ a<=2 a==2 a>=2 a>2 -({"xy","abc"})[2] -("abc"):sub(2,2) +({"xy",b})[2] +L[#L] +L[#L-1] +b:sub(2,2) +b:sub(-2,-2) +("abcdefg"):sub(3,5) ("a"):byte(1) -("abc"):byte(2) +b:byte(2) string.char(99) b.."xyz" -("abc"):len() +b:len() ""..5 1*"5" b:gsub("a","A") b:gsub("%(","*") b:gsub(b:gsub("(%W)","%%%1"),b:gsub("%%","%%%%")) -table.concat({"xy","abc"},"/") +table.concat({"xy",b},"/") table.concat({"12","345"}) +a==2 and a or 3 +(a==2 and{c}or{false})[1] ``` ## Parentheses ```polygolf $t <- "abc"; -text_byte_length $t; +$i <- 0; +# $t; +ord_at[byte] "abc" $i; ``` ```lua nogolf t="abc" +i=0 t:len() +("abc"):byte(1+i) ``` ```polygolf diff --git a/src/languages/lua/plugins.ts b/src/languages/lua/plugins.ts index e28ca99b..b2f294f9 100644 --- a/src/languages/lua/plugins.ts +++ b/src/languages/lua/plugins.ts @@ -3,29 +3,21 @@ import { annotate, builtin, integerType, - isIntLiteral, + isInt, isOp, } from "../../IR"; -import { type Plugin } from "../../common/Language"; -export const base10DecompositionToFloatLiteralAsBuiltin: Plugin = { - name: "base10DecompositionToFloatLiteralAsBuiltin", - visit(node) { - let k = 1n; - let pow: Node = node; - if (isOp("mul")(node) && isIntLiteral()(node.args[0])) { - k = node.args[0].value; - pow = node.args[1]; - } +export function base10DecompositionToFloatLiteralAsBuiltin(node: Node) { + let k = 1n; + let pow: Node = node; + if (isOp("mul")(node) && isInt()(node.args[0])) { + k = node.args[0].value; + pow = node.args[1]; + } - if ( - isOp("pow")(pow) && - isIntLiteral(10n)(pow.args[0]) && - isIntLiteral()(pow.args[1]) - ) { - const e = pow.args[1].value; - const value = k * 10n ** e; - return annotate(builtin(`${k}e${e}`), integerType(value, value)); - } - }, -}; + if (isOp("pow")(pow) && isInt(10n)(pow.args[0]) && isInt()(pow.args[1])) { + const e = pow.args[1].value; + const value = k * 10n ** e; + return annotate(builtin(`${k}e${e}`), integerType(value, value)); + } +} diff --git a/src/languages/nim/emit.ts b/src/languages/nim/emit.ts index 4aed456d..2a0d5985 100644 --- a/src/languages/nim/emit.ts +++ b/src/languages/nim/emit.ts @@ -5,7 +5,7 @@ import { EmitError, emitIntLiteral, } from "../../common/emit"; -import { type Array, type IR, isIdent, isIntLiteral, isText } from "../../IR"; +import { type Array, type IR, isIdent, isInt, isText } from "../../IR"; import { type CompilationContext } from "@/common/compile"; const emitNimText = emitTextFactory( @@ -25,20 +25,22 @@ const emitNimText = emitTextFactory( function precedence(expr: IR.Node): number { switch (expr.kind) { + case "FunctionCall": + return 12; case "Prefix": return 11; case "Infix": return binaryPrecedence(expr.name); - case "FunctionCall": - return 2; - case "MethodCall": - return 12; + case "ConditionalOp": + return -Infinity; } return Infinity; } function binaryPrecedence(opname: string): number { switch (opname) { + case ".": + return 12; case "^": return 10; case "*": @@ -62,12 +64,15 @@ function binaryPrecedence(opname: string): number { case "!=": case ">=": case ">": + case "in": return 5; case "and": return 4; case "or": case "xor": return 3; + case " ": + return 1; } throw new Error( `Programming error - unknown Nim binary operator '${opname}.'`, @@ -154,8 +159,8 @@ export default function emitProgram( emitMultiNode(e.body), ]; case "ForRange": { - const start = isIntLiteral(0n)(e.start) ? [] : emit(e.start); - if (isIntLiteral(1n)(e.increment)) { + const start = isInt(0n)(e.start) ? [] : emit(e.start); + if (isInt(1n)(e.increment)) { return [ "for", e.variable === undefined ? "()" : emit(e.variable), @@ -219,6 +224,16 @@ export default function emitProgram( return [joinNodes(",", e.variables), "=", emit(e.expr)]; case "MutatingInfix": return [emit(e.variable), "$GLUE$", e.name + "=", emit(e.right)]; + case "ConditionalOp": + return [ + "if", + emit(e.condition, 0), + ":", + emit(e.consequent, 0), + "else", + ":", + emit(e.alternate, 0), + ]; case "Identifier": return e.name; case "Text": @@ -226,56 +241,29 @@ export default function emitProgram( case "Integer": return emitIntLiteral(e, { 10: ["", ""], 16: ["0x", ""] }); case "FunctionCall": - if (isIdent()(e.func) && e.args.length === 1 && isText()(e.args[0])) { + return [emit(e.func), "$GLUE$", "(", joinNodes(",", e.args), ")"]; + case "Infix": { + const rightAssoc = e.name === "^" || e.name === " "; + if ( + e.name === " " && + isText()(e.right) && + (isIdent()(e.left) || + (e.left.kind === "Infix" && e.left.name === ".")) + ) { const [low, high] = context.options.codepointRange; if (low === 1 && high === Infinity) { - const raw = emitAsRawText(e.args[0].value, e.func.name); + const raw = emitAsRawText(e.right.value, ""); if (raw !== null) { + const res = [ + emit(e.left, prec + (rightAssoc ? 1 : 0)), + "$GLUE$", + raw, + ]; prec = Infinity; - return raw; + return res; } } } - if (e.args.length > 1) { - prec = 11.5; - } - if (e.args.length > 1 || e.args.length === 0) - return [emit(e.func), "$GLUE$", "(", joinNodes(",", e.args), ")"]; - return [emit(e.func), joinNodes(",", e.args)]; - case "MethodCall": - if (e.args.length > 1) - return [ - emit(e.object, prec), - ".", - e.ident.name, - e.args.length > 0 - ? ["$GLUE$", "(", joinNodes(",", e.args), ")"] - : [], - ]; - else { - const [low, high] = context.options.codepointRange; - if ( - e.args.length === 1 && - isText()(e.args[0]) && - low === 1 && - high === Infinity - ) { - const raw = emitAsRawText(e.args[0].value, e.ident.name); - if (raw !== null) { - prec = 12; - return [emit(e.object, prec), ".", raw]; - } - } - prec = 2; - return [ - emit(e.object, precedence(e)), - ".", - e.ident.name, - e.args.length > 0 ? joinNodes(",", e.args) : [], - ]; - } - case "Infix": { - const rightAssoc = e.name === "^"; return [ emit(e.left, prec + (rightAssoc ? 1 : 0)), /[A-Za-z]/.test(e.name[0]) ? [] : "$GLUE$", @@ -302,6 +290,8 @@ export default function emitProgram( ]; } return ["[", joinNodes(",", e.exprs), "]"]; + case "Set": + return ["[", joinNodes(",", e.exprs), "]", ".", "toSet"]; case "Table": return [ "{", @@ -314,13 +304,12 @@ export default function emitProgram( "toTable", ]; case "IndexCall": - if (e.oneIndexed) throw new EmitError(expr, "one indexed"); - return [emit(e.collection, 12), "[", emit(e.index), "]"]; + return [emit(e.collection, 12), "$GLUE$", "[", emit(e.index), "]"]; case "RangeIndexCall": - if (e.oneIndexed) throw new EmitError(expr, "one indexed"); - if (!isIntLiteral(1n)(e.step)) throw new EmitError(expr, "step"); + if (!isInt(1n)(e.step)) throw new EmitError(expr, "step"); return [ emit(e.collection, 12), + "$GLUE$", "[", emit(e.low), "..<", diff --git a/src/languages/nim/index.ts b/src/languages/nim/index.ts index 0b760524..b13e5ba7 100644 --- a/src/languages/nim/index.ts +++ b/src/languages/nim/index.ts @@ -3,11 +3,16 @@ import { indexCall, int, rangeIndexCall, - add1, + succ, array, isText, builtin, op, + prefix, + text, + assignment, + isIdent, + infix, } from "../../IR"; import { defaultDetokenizer, @@ -20,14 +25,22 @@ import { import emitProgram from "./emit"; import { mapOps, - mapToPrefixAndInfix, + mapUnaryAndBinary, useIndexCalls, flipBinaryOps, removeImplicitConversions, printIntToPrint, mapTo, + backwardsIndexToForwards, } from "../../plugins/ops"; -import { addNimImports, useUFCS, useUnsignedDivision } from "./plugins"; +import { + addNimImports, + getEndIndex, + removeSystemNamespace, + useBackwardsIndex, + useUFCS, + useUnsignedDivision, +} from "./plugins"; import { alias, renameIdents } from "../../plugins/idents"; import { forArgvToForEach, @@ -38,8 +51,17 @@ import { removeUnusedForVar, shiftRangeOneUp, } from "../../plugins/loops"; -import { golfStringListLiteral, listOpsToTextOps } from "../../plugins/static"; -import { golfLastPrint, implicitlyConvertPrintArg } from "../../plugins/print"; +import { + golfStringListLiteral, + hardcode, + listOpsToTextOps, +} from "../../plugins/static"; +import { + golfLastPrint, + implicitlyConvertPrintArg, + putcToPrintChar, + mergePrint, +} from "../../plugins/print"; import { useDecimalConstantPackedPrinter, useLowDecimalListPackedPrinter, @@ -49,8 +71,9 @@ import hash from "./hash"; import { textToIntToTextGetToInt, textToIntToFirstIndexTextGetToInt, - useEquivalentTextOp, + usePrimaryTextOps, useMultireplace, + startsWithEndsWithToSliceEquality, } from "../../plugins/textOps"; import { assertInt64 } from "../../plugins/types"; import { @@ -72,19 +95,22 @@ import { pickAnyInt, lowBitsPlugins, } from "../../plugins/arithmetic"; +import { safeConditionalOpToAt } from "../../plugins/conditions"; const nimLanguage: Language = { name: "Nim", extension: "nim", emitter: emitProgram, phases: [ - required(printIntToPrint), + search(hardcode()), + required(printIntToPrint, putcToPrintChar, usePrimaryTextOps("byte")), + simplegolf(golfLastPrint()), search( + mergePrint, flipBinaryOps, golfStringListLiteral(), - listOpsToTextOps("text_byte_find", "text_get_byte"), - golfLastPrint(), - forRangeToForEach("array_get", "list_get", "text_get_byte"), + listOpsToTextOps("find[byte]", "at[byte]"), + forRangeToForEach("at[Array]", "at[List]", "at[byte]"), tempVarToMultipleAssignment, useDecimalConstantPackedPrinter, useLowDecimalListPackedPrinter, @@ -104,32 +130,48 @@ const nimLanguage: Language = { forArgvToForRange(true), ...truncatingOpsPlugins, decomposeIntLiteral(), + startsWithEndsWithToSliceEquality("byte"), ), + simplegolf(safeConditionalOpToAt("Array")), required( pickAnyInt, forArgvToForEach, ...truncatingOpsPlugins, - useIndexCalls(), - useEquivalentTextOp(true, false), mapOps({ argv: func("commandLineParams"), - argv_get: (x) => func("paramStr", add1(x[0])), + "at[argv]": (x) => func("paramStr", succ(x[0])), }), removeUnusedForVar, forRangeToForRangeInclusive(true), implicitlyConvertPrintArg, textToIntToFirstIndexTextGetToInt, + useUnsignedDivision, + useBackwardsIndex, + backwardsIndexToForwards(false), + useIndexCalls(), mapOps({ - text_get_byte_to_int: (x) => func("ord", op("text_get_byte", ...x)), - read_line: func("readLine", builtin("stdin")), + "reversed[codepoint]": (x) => + op.join(func("reversed", func("toRunes", x)), text("")), + "reversed[byte]": (x) => op.join(func("reversed", x[0]), text("")), + }), + mapOps({ + "char[codepoint]": (x) => prefix("$", func("Rune", x)), + "ord_at[byte]": (x) => func("ord", op["at[byte]"](x[0], x[1])), + "ord_at[codepoint]": (x) => + func("ord", op["at[codepoint]"](x[0], x[1])), + "read[line]": func("readLine", builtin("stdin")), join: (x) => func("join", isText("")(x[1]) ? [x[0]] : x), true: builtin("true"), false: builtin("false"), - text_get_byte: (x) => indexCall(x[0], x[1]), - text_get_byte_slice: (x) => rangeIndexCall(x[0], x[1], x[2], int(1n)), - print: (x) => func("write", builtin("stdout"), x), - text_replace: (x) => - func("replace", isText("")(x[2]) ? [x[0], x[1]] : x), + "at[byte]": (x) => indexCall(x[0], x[1]), + "at[codepoint]": (x) => + prefix("$", indexCall(func("toRunes", x[0]), x[1])), + "slice[byte]": (x) => + rangeIndexCall(x[0], x[1], getEndIndex(x[1], x[2]), int(1n)), + "slice[List]": (x) => + rangeIndexCall(x[0], x[1], getEndIndex(x[1], x[2]), int(1n)), + "print[Text]": (x) => func("write", builtin("stdout"), x), + replace: (x) => func("replace", isText("")(x[2]) ? [x[0], x[1]] : x), text_multireplace: (x) => func( "multireplace", @@ -140,28 +182,53 @@ const nimLanguage: Language = { ), // Polygolf doesn't have array of tuples, so we use array of arrays instead ), ), + "size[codepoint]": (x) => op["size[List]"](func("toRunes", x)), + push: (x) => + isIdent()(x[0]) ? assignment(x[0], op.append(x[0], x[1])) : undefined, + int_to_bool: (x) => op["eq[Int]"](x[0], int(0n)), + int_to_bin_aligned: (x) => + func("align", op.int_to_bin(x[0]), x[1], text("0")), + int_to_hex_aligned: (x) => + func("align", op.int_to_hex(x[0]), x[1], text("0")), }), mapTo(func)({ - text_split: "split", - text_split_whitespace: "split", - text_byte_length: "len", + gcd: "gcd", + split: "split", + split_whitespace: "split", + "size[byte]": "len", + "size[List]": "len", + "size[Table]": "len", repeat: "repeat", max: "max", min: "min", abs: "abs", - text_to_int: "parseInt", - println: "echo", + dec_to_int: "parseInt", + "println[Text]": "echo", bool_to_int: "int", - int_to_text_byte: "chr", - list_find: "find", + "char[byte]": "chr", + "find[List]": "system.find", + "find[byte]": "find", + "sorted[Int]": "sorted", + "sorted[Ascii]": "sorted", + "reversed[List]": "reversed", + int_to_bin: "toBin", + int_to_hex: "toHex", + right_align: "align", + starts_with: "startsWith", + ends_with: "endsWith", }), - useUnsignedDivision, - mapToPrefixAndInfix( + mapTo((x: string, [right, left]) => infix(x, left, right))({ + "contains[Array]": "system.in", + "contains[List]": "system.in", + "contains[Text]": "in", + "contains[Table]": "system.in", + }), + mapUnaryAndBinary( { bit_not: "not", not: "not", neg: "-", - int_to_text: "$", + int_to_dec: "$", pow: "^", mul: "*", trunc_div: "div", @@ -172,11 +239,15 @@ const nimLanguage: Language = { bit_shift_right: "shr", add: "+", sub: "-", - concat: "&", + "concat[Text]": "&", + "concat[List]": "&", + append: "&", lt: "<", leq: "<=", - eq: "==", - neq: "!=", + "eq[Int]": "==", + "eq[Text]": "==", + "neq[Int]": "!=", + "neq[Text]": "!=", geq: ">=", gt: ">", and: "and", @@ -185,9 +256,8 @@ const nimLanguage: Language = { bit_or: "or", bit_xor: "xor", }, - ["+", "*", "%%", "/%", "-", "&"], + ["+", "*", "-", "&"], ), - useUnsignedDivision, addNimImports, ), simplegolf( @@ -209,8 +279,9 @@ const nimLanguage: Language = { noStandaloneVarDeclarations, assertInt64, removeImplicitConversions, - useUFCS, + removeSystemNamespace, ), + search(useUFCS), ], detokenizer: defaultDetokenizer((a, b) => { const left = a[a.length - 1]; @@ -223,8 +294,9 @@ const nimLanguage: Language = { if ( /[A-Za-z]/.test(left) && - !["var", "in", "else", "if", "while", "for"].includes(a) && - (symbols + `"({`).includes(right) && + ((!["var", "in", "else", "if", "while", "for"].includes(a) && + (symbols + `"({[`).includes(right)) || + right === `"`) && !["=", ":", ".", "::"].includes(b) ) return true; // identifier meeting an operator or string literal or opening paren diff --git a/src/languages/nim/nim.test.md b/src/languages/nim/nim.test.md index 740df11f..32534b62 100644 --- a/src/languages/nim/nim.test.md +++ b/src/languages/nim/nim.test.md @@ -4,17 +4,21 @@ ```polygolf $t:(Ascii 3) <- ""; +$t2:Text <- ""; $n:0..1 <- 0; $m <- $n; $b <- (1<2); text_get_byte $t 2; +text_get_codepoint $t 2; text_get_byte_slice $t 2 6; text_byte_to_int "a"; text_get_byte_to_int "abc" 1; text_split $t "|"; text_split_whitespace $t; text_byte_length $t; +slice $t -10 3; +slice $t -10 10; repeat $t 3; max $n 1; min $n 1; @@ -24,7 +28,9 @@ print $t; println $t; bool_to_int $b; int_to_text_byte 48; +int_to_codepoint 48; $t .. "x"; +$t @ -1; text_replace "a+b+c" "+" "*"; text_replace "a*b*c" "*" ""; text_multireplace "XYZXYZ" "Y" "b" "X" "a"; @@ -36,6 +42,10 @@ not $b; - $n; int_to_text $n; +int_to_bin 3; +int_to_bin_aligned 3 5; +int_to_hex 3; +int_to_hex_aligned 3 5; $n ^ 3; $n * $m; -3 trunc_div $n; @@ -61,41 +71,52 @@ and $b $b; or $b $b; list_find (list "") ""; +starts_with $t $t2; +ends_with $t $t2; ``` ```nim nogolf -import strutils,math +import unicode,strutils,math var - t="" + t,T="" n=0 m=n b=1<2 t[2] -t[2..<6] -"a"[0].ord -"abc"[1].ord -t.split"|" -t.split -t.len -t.repeat 3 -1.max n -1.min n -n.abs -t.parseInt -stdout.write t -t.echo -b.int -48.chr +$toRunes(t)[2] +t[2..<8] +ord("a"[0]) +ord("abc"[1]) +split(t,"|") +split(t) +len(t) +t[^10..< ^7] +t[^10..< ^0] +repeat(t,3) +max(1,n) +min(1,n) +abs(n) +parseInt(t) +write(stdout,t) +echo(t) +int(b) +chr(48) +$Rune(48) t&"x" -"a+b+c".replace("+","*") -"a*b*c".replace"*" -"XYZXYZ".multireplace {"Y":"b","X":"a"} -@["xy","abc"].join"/" -@["12","345"].join +t[^1] +replace("a+b+c","+","*") +replace("a*b*c","*") +multireplace("XYZXYZ",{"Y":"b","X":"a"}) +join(@["xy","abc"],"/") +join(@["12","345"]) not n not b -n $n +toBin(3) +align(toBin(3),5,"0") +toHex(3) +align(toHex(3),5,"0") n^3 n*m -3 div n @@ -117,35 +138,18 @@ n>=3 n>3 b and b b or b -@[""].find"" +find(@[""],"") +startsWith(t,T) +endsWith(t,T) ``` ## Misc -```polygolf -print (list_get (text_split "abc" "b") 0); -``` - -```nim -include re -"abc".split"b"[0].echo -``` - -```polygolf -$a:0..1 <- 0; -println_int (($a + 1) * $a); -``` - -```nim nogolf -var a=0 -echo (1+a)*a -``` - ```polygolf println ((int_to_text 1) .. "x"); ``` -```nim nogolf +```nim no:hardcode echo $1&"x" ``` @@ -182,6 +186,17 @@ for i in..9: a=i ``` +```polygolf +$e <- "abc"; +for $i 0 3 { + println (at[byte] $e $i); +}; +``` + +```nim +for i in "abc":echo i +``` + ## Argv ```polygolf @@ -192,7 +207,7 @@ for_argv $x 100 { ```nim import os -for x in..99:(paramStr 1+x).echo +for x in..99:echo paramStr 1+x ``` ```polygolf @@ -204,7 +219,7 @@ for $i $b 16 { ```nim nogolf var b=0 -for i in b..<16:i.echo +for i in b..<16:echo(i) ``` ## Variables & Assignments @@ -275,6 +290,11 @@ list_get (text_split "a b" " ") 1; ```nim nogolf include re +split("a b"," ")[1] +``` + +```nim +include re "a b".split" "[1] ``` @@ -339,3 +359,43 @@ for()in..9:echo"Hi" "\u0161" "\u{1f48e}" ``` + +## Conditional ops + +```polygolf +(conditional (builtin "c"):Bool 3 4) div 2; +``` + +```nim nogolf +(if c:3 else:4)/%2 +``` + +```nim +[4,3][int c]/%2 +``` + +```polygolf +$c:Bool <- true; +2 + (conditional $c 3 4); +``` + +TODO split left & right prec after #254 + +```nim nogolf skip +var c=true +2+if c:3 else:4 +``` + +## Ufcs + +```polygolf +function_call (infix "." (builtin "x") (builtin "f")) (builtin "y"); +infix " " (builtin "f") (builtin "x"); +index_call (infix " " (infix "." "x" (builtin "f")) " ") 1; +``` + +```nim skipTypecheck +x.f y +f x +"x".f" "[1] +``` diff --git a/src/languages/nim/plugins.ts b/src/languages/nim/plugins.ts index a7036b4d..49bfa61d 100644 --- a/src/languages/nim/plugins.ts +++ b/src/languages/nim/plugins.ts @@ -1,17 +1,22 @@ import { + functionCall, + type Node, importStatement, + infix, integerType, isIdent, - isOfKind, isOp, isSubtype, - isText, - methodCall, op, + prefix, + type Op, + type OpCode, } from "../../IR"; import { getType } from "../../common/getType"; -import { type Plugin } from "../../common/Language"; +import type { Plugin } from "../../common/Language"; import { addImports } from "../../plugins/imports"; +import type { Spine } from "../../common/Spine"; +import { replaceAtIndex } from "../../common/arrays"; const includes: [string, string[]][] = [ ["re", ["strutils"]], @@ -38,15 +43,28 @@ const includes: [string, string[]][] = [ export const addNimImports: Plugin = addImports( { "^": "math", + gcd: "math", repeat: "strutils", replace: "strutils", multireplace: "strutils", join: "strutils", + find: "strutils", + in: "strutils", + toBin: "strutils", + toHex: "strutils", + align: "strutils", paramStr: "os", commandLineParams: "os", split: "strutils", hash: "hashes", Table: "tables", + Set: "sets", + toRunes: "unicode", + Rune: "unicode", + sorted: "algorithm", + reversed: "algorithm", + startsWith: "strutils", + endsWith: "strutils", }, (modules: string[]) => { if (modules.length < 1) return; @@ -60,29 +78,64 @@ export const addNimImports: Plugin = addImports( }, ); -export const useUnsignedDivision: Plugin = { - name: "useUnsignedDivision", - visit(node, spine) { - if (isOp("trunc_div", "rem")(node)) { - return isSubtype(getType(node.args[0], spine), integerType(0)) && - isSubtype(getType(node.args[0], spine), integerType(0)) - ? op(`unsigned_${node.op}`, ...node.args) - : undefined; - } - }, -}; +export function useUnsignedDivision(node: Node, spine: Spine) { + if (isOp("trunc_div", "rem")(node)) { + return isSubtype(getType(node.args[0], spine), integerType(0)) && + isSubtype(getType(node.args[0], spine), integerType(0)) + ? op[`unsigned_${node.op}`](...node.args) + : undefined; + } +} -export const useUFCS: Plugin = { - name: "useUFCS", - visit(node) { - if (node.kind === "FunctionCall" && node.args.length > 0) { - if (node.args.length === 1 && isText()(node.args[0])) { - return; - } - const [obj, ...args] = node.args; - if (!isOfKind("Infix", "Prefix")(obj) && isIdent()(node.func)) { - return methodCall(obj, node.func, ...args); - } +export function useUFCS(node: Node) { + if (node.kind === "FunctionCall") { + if (node.args.length === 1) { + return infix(" ", node.func, node.args[0]); } - }, -}; + if (node.args.length > 1 && isIdent()(node.func)) { + return functionCall( + infix(".", node.args[0], node.func), + ...node.args.slice(1), + ); + } + } + if (node.kind === "Infix" && node.name === " " && isIdent()(node.left)) { + return infix(".", node.right, node.left); + } +} + +export function useBackwardsIndex(node: Node, spine: Spine) { + if ( + isOp()(node) && + (node.op.includes("at_back") || node.op.includes("slice_back")) + ) { + return op.unsafe( + node.op, + ...replaceAtIndex( + node.args, + 1, + prefix( + "system.^", + op.neg((node as Op<`${string}at_back${string}` & OpCode>).args[1]), + ), + ), + ); + } +} + +export function getEndIndex(start: Node, length: Node) { + if (start.kind === "Prefix" && start.name === "system.^") { + return prefix(start.name, op.sub(start.arg, length)); + } + return op.add(start, length); +} + +export function removeSystemNamespace(node: Node, spine: Spine) { + if ( + "name" in node && + node.kind !== "FunctionDefinition" && + node.name.startsWith("system.") + ) { + return { ...node, name: node.name.slice("system.".length) }; + } +} diff --git a/src/languages/polygolf/emit.ts b/src/languages/polygolf/emit.ts index 436cefc7..9281faab 100644 --- a/src/languages/polygolf/emit.ts +++ b/src/languages/polygolf/emit.ts @@ -5,11 +5,18 @@ import { type Node, id, type IR, - isIntLiteral, + isInt, text, toString, variants, type Variants, + opCodeDefinitions, + type OpCode, + isOpCode, + isNullary, + infixableOpCodeNames, + assignment, + op as opNode, } from "../../IR"; /* @@ -90,32 +97,50 @@ function emitNodeWithoutAnnotation( indent = false, ): TokenTree { function emitSexpr(op: string, ...args: (TokenTree | Node)[]): TokenTree { - const isNullary = [ - "argv", - "argc", - "true", - "false", - "read_codepoint", - "read_byte", - "read_int", - "read_line", - ].includes(op); - if (op === "@") op = expr.kind; - op = op - .split(/\.?(?=[A-Z])/) - .join("_") - .toLowerCase(); + let nullary = false; + if (op === "@") { + op = expr.kind; + op = op + .split(/\.?(?=[A-Z])/) + .join("_") + .toLowerCase(); + } else { + if (isOpCode(op)) { + nullary = isNullary(op); + if ( + "front" in opCodeDefinitions[op as OpCode] && + typeof (opCodeDefinitions[op as OpCode] as any).front === "string" + ) + op = (opCodeDefinitions[op as OpCode] as any).front; + } + } + if (op === "set_at") { + return emitNodeWithoutAnnotation( + assignment( + opNode["at[Ascii]"](args[0] as Node, args[1] as Node) as any, + args[2] as Node, + ), + asStatement, + indent, + ); + } + const result: TokenTree = []; - if (!asStatement && !isNullary) result.push("("); + if (!asStatement && !nullary) result.push("("); if (indent) result.push("$INDENT$", "\n"); - if (opAliases[op] !== undefined && args.length === 2) { + + if ( + (op === "<-" || + op === "=>" || + infixableOpCodeNames.includes(op as any)) && + args.length === 2 + ) { let a = args[0]; result.push(typeof a === "string" || !("kind" in a) ? a : emitNode(a)); - result.push(opAliases[op]); + result.push(op); a = args[1]; result.push(typeof a === "string" || !("kind" in a) ? a : emitNode(a)); } else { - op = opAliases[op] ?? op; result.push(op); result.push( joinTrees( @@ -128,7 +153,7 @@ function emitNodeWithoutAnnotation( } if (!asStatement) { if (indent) result.push("$DEDENT$", "\n"); - if (!isNullary) result.push(")"); + if (!nullary) result.push(")"); } return result; } @@ -141,13 +166,13 @@ function emitNodeWithoutAnnotation( case "Variants": return emitVariants(expr, indent); case "KeyValue": - return emitSexpr("key_value", expr.key, expr.value); + return emitSexpr("=>", expr.key, expr.value); case "Function": return emitSexpr("func", ...expr.args, expr.expr); case "Op": return emitSexpr(expr.op, ...expr.args); case "Assignment": - return emitSexpr("assign", expr.variable, expr.expr); + return emitSexpr("<-", expr.variable, expr.expr); case "FunctionCall": { const id = emitNode(expr.func); if (typeof id === "string" && id.startsWith("$")) { @@ -157,7 +182,7 @@ function emitNodeWithoutAnnotation( } case "Identifier": if (expr.builtin) { - return emitSexpr("Builtin", text(expr.name)); + return emitSexpr("builtin", text(expr.name)); } else if (/^\w+$/.test(expr.name)) { return "$" + expr.name; } @@ -199,9 +224,9 @@ function emitNodeWithoutAnnotation( ); } let args: Node[] = []; - if (!isIntLiteral(1n)(expr.increment)) args = [expr.increment, ...args]; + if (!isInt(1n)(expr.increment)) args = [expr.increment, ...args]; args = [expr.end, ...args]; - if (!isIntLiteral(0n)(expr.start) || args.length > 1) + if (!isInt(0n)(expr.start) || args.length > 1) args = [expr.start, ...args]; if (expr.variable !== undefined || args.length > 1) args = [expr.variable ?? id("_"), ...args]; @@ -243,19 +268,9 @@ function emitNodeWithoutAnnotation( case "MutatingInfix": return emitSexpr("@", text(expr.name), expr.variable, expr.right); case "IndexCall": - return emitSexpr( - expr.oneIndexed ? "IndexCallOneIndexed" : "@", - expr.collection, - expr.index, - ); + return emitSexpr("@", expr.collection, expr.index); case "RangeIndexCall": - return emitSexpr( - expr.oneIndexed ? "RangeIndexCallOneIndexed" : "@", - expr.collection, - expr.low, - expr.high, - expr.step, - ); + return emitSexpr("@", expr.collection, expr.low, expr.high, expr.step); case "MethodCall": return emitSexpr("@", expr.object, text(expr.ident.name), ...expr.args); case "PropertyCall": @@ -324,29 +339,3 @@ function emitNodeWithoutAnnotation( return emitSexpr("@", expr.func, ...expr.args); } } - -const opAliases: Record = { - add: "+", - neg: "-", - sub: "-", - mul: "*", - pow: "^", - bit_and: "&", - bit_or: "|", - bit_xor: "~", - bit_not: "~", - bit_shift_left: "<<", - bit_shift_right: ">>", - eq: "==", - neq: "!=", - leq: "<=", - lt: "<", - geq: ">=", - gt: ">", - list_length: "#", - concat: "..", - assign: "<-", - key_value: "=>", - mod: "mod", - div: "div", -}; diff --git a/src/languages/python/emit.ts b/src/languages/python/emit.ts index 139c139b..6c0bf202 100644 --- a/src/languages/python/emit.ts +++ b/src/languages/python/emit.ts @@ -1,4 +1,4 @@ -import { charLength } from "../../common/objective"; +import { charLength } from "../../common/strings"; import { type TokenTree } from "@/common/Language"; import { containsMultiNode, @@ -7,7 +7,7 @@ import { emitTextFactory, joinTrees, } from "../../common/emit"; -import { type IR, isIntLiteral, text, isText, id, infix } from "../../IR"; +import { type IR, isInt, text, isText, id, infix } from "../../IR"; import { type CompilationContext } from "@/common/compile"; export const emitPythonText = emitTextFactory( @@ -29,6 +29,8 @@ function precedence(expr: IR.Node): number { return unaryPrecedence(expr.name); case "Infix": return binaryPrecedence(expr.name); + case "ConditionalOp": + return 0; } return Infinity; } @@ -59,6 +61,7 @@ function binaryPrecedence(opname: string): number { case "!=": case ">=": case ">": + case "in": return 4; case "and": return 2; @@ -137,10 +140,10 @@ export default function emitProgram( ]; case "ForRange": { const start = emit(e.start); - const start0 = isIntLiteral(0n)(e.start); + const start0 = isInt(0n)(e.start); const end = emit(e.end); const increment = emit(e.increment); - const increment1 = isIntLiteral(1n)(e.increment); + const increment1 = isInt(1n)(e.increment); return e.variable === undefined && start0 && increment1 ? [ "for", @@ -201,6 +204,14 @@ export default function emitProgram( 16: ["0x", ""], 36: ["int('", "',36)"], }); + case "ConditionalOp": + return [ + emit(e.consequent, prec + 1), + "if", + emit(e.condition, prec + 1), + "else", + emit(e.alternate, prec), + ]; case "FunctionCall": return [ emit(e.func), @@ -224,6 +235,8 @@ export default function emitProgram( } case "Prefix": return [e.name, emit(e.arg, prec)]; + case "Set": + return ["{", joinNodes(",", e.exprs), "}"]; case "List": return ["[", joinNodes(",", e.exprs), "]"]; case "Table": @@ -236,21 +249,20 @@ export default function emitProgram( "}", ]; case "IndexCall": - if (e.oneIndexed) throw new EmitError(expr, "one indexed"); return [emit(e.collection, Infinity), "[", emit(e.index), "]"]; case "RangeIndexCall": { - if (e.oneIndexed) throw new EmitError(expr, "one indexed"); const low = emit(e.low); - const low0 = isIntLiteral(0n)(e.low); + const low0 = isInt(0n)(e.low); const high = emit(e.high); + const high0 = isInt(0n)(e.high); const step = emit(e.step); - const step1 = isIntLiteral(1n)(e.step); + const step1 = isInt(1n)(e.step); return [ emit(e.collection, Infinity), "[", - ...(low0 ? [] : low), + low0 ? [] : low, ":", - high, + high0 ? [] : high, step1 ? [] : [":", ...step], "]", ]; diff --git a/src/languages/python/index.ts b/src/languages/python/index.ts index 83618655..4dbf7beb 100644 --- a/src/languages/python/index.ts +++ b/src/languages/python/index.ts @@ -9,12 +9,15 @@ import { listType, textType, namedArg, - add1, + succ, table, keyValue, type Text, builtin, isText, + implicitConversion, + infix, + list, } from "../../IR"; import { type Language, @@ -26,12 +29,14 @@ import { import emitProgram, { emitPythonText } from "./emit"; import { mapOps, - mapToPrefixAndInfix, + mapUnaryAndBinary, useIndexCalls, removeImplicitConversions, methodsAsFunctions, printIntToPrint, mapTo, + arraysToLists, + backwardsIndexToForwards, } from "../../plugins/ops"; import { alias, renameIdents } from "../../plugins/idents"; import { @@ -40,8 +45,16 @@ import { forRangeToForRangeOneStep, removeUnusedForVar, } from "../../plugins/loops"; -import { golfStringListLiteral, listOpsToTextOps } from "../../plugins/static"; -import { golfLastPrint, implicitlyConvertPrintArg } from "../../plugins/print"; +import { + golfStringListLiteral, + hardcode, + listOpsToTextOps, +} from "../../plugins/static"; +import { + golfLastPrint, + implicitlyConvertPrintArg, + putcToPrintChar, +} from "../../plugins/print"; import { packSource2to1, packSource3to1, @@ -51,8 +64,9 @@ import { import { textGetToIntToTextGet, textToIntToTextGetToInt, - useEquivalentTextOp, + usePrimaryTextOps, useMultireplace, + startsWithEndsWithToSliceEquality, } from "../../plugins/textOps"; import { addOneToManyAssignments, @@ -67,23 +81,27 @@ import { equalityToInequality, lowBitsPlugins, pickAnyInt, + useImplicitBoolToInt, useIntegerTruthiness, } from "../../plugins/arithmetic"; import { tableToListLookup } from "../../plugins/tables"; -import { charLength } from "../../common/objective"; +import { charLength } from "../../common/strings"; +import { golfTextListLiteralIndex } from "./plugins"; +import { safeConditionalOpToAt } from "../../plugins/conditions"; const pythonLanguage: Language = { name: "Python", extension: "py", emitter: emitProgram, phases: [ - required(printIntToPrint), + search(hardcode()), + required(printIntToPrint, arraysToLists, usePrimaryTextOps("codepoint")), + simplegolf(golfLastPrint()), search( golfStringListLiteral(), - listOpsToTextOps("text_codepoint_find", "text_get_codepoint"), + listOpsToTextOps("find[codepoint]", "at[codepoint]"), tempVarToMultipleAssignment, - forRangeToForEach("array_get", "list_get", "text_get_codepoint"), - golfLastPrint(), + forRangeToForEach("at[List]", "at[codepoint]"), equalityToInequality, useDecimalConstantPackedPrinter, useLowDecimalListPackedPrinter, @@ -97,52 +115,92 @@ const pythonLanguage: Language = { useMultireplace(true), inlineVariables, forArgvToForEach, - useEquivalentTextOp(false, true), - useIndexCalls(), decomposeIntLiteral(), + startsWithEndsWithToSliceEquality("codepoint"), ), + simplegolf(safeConditionalOpToAt("List")), required( pickAnyInt, forArgvToForEach, removeUnusedForVar, - useEquivalentTextOp(false, true), + putcToPrintChar, mapOps({ argv: builtin("sys.argv[1:]"), - argv_get: (x) => - op( - "list_get", + "at[argv]": (x) => + op["at[List]"]( { ...builtin("sys.argv"), type: listType(textType()) }, - add1(x[0]), + succ(x[0]), ), }), - useIndexCalls(), + useImplicitBoolToInt, + backwardsIndexToForwards(false), + useIndexCalls(), + ), + simplegolf(golfTextListLiteralIndex), + required( textGetToIntToTextGet, implicitlyConvertPrintArg, mapOps({ true: int(1), false: int(0), - list_find: (x) => method(x[0], "index", x[1]), + "find[List]": (x) => method(x[0], "index", x[1]), + "find[codepoint]": (x) => method(x[0], "find", x[1]), + "find[byte]": (x) => + method( + func("bytes", x[0], text("u8")), + "find", + func("bytes", x[1], text("u8")), + ), join: (x) => method(x[1], "join", x[0]), - - text_codepoint_reversed: (x) => + "size[byte]": (x) => func("len", func("bytes", x[0], text("u8"))), + "reversed[codepoint]": (x) => rangeIndexCall(x[0], builtin(""), builtin(""), int(-1)), - text_get_codepoint: (x) => indexCall(x[0], x[1]), - - text_get_codepoint_slice: (x) => - rangeIndexCall(x[0], x[1], add1(x[2]), int(1)), - text_split: (x) => method(x[0], "split", x[1]), - text_split_whitespace: (x) => method(x[0], "split"), + "reversed[byte]": (x) => + method( + rangeIndexCall( + func("bytes", x[0], text("u8")), + builtin(""), + builtin(""), + int(-1), + ), + "decode", + text("u8"), + ), + "reversed[List]": (x) => + rangeIndexCall(x[0], builtin(""), builtin(""), int(-1)), + "at[codepoint]": (x) => indexCall(x[0], x[1]), + "at[byte]": (x) => op["char[byte]"](op["ord_at[byte]"](x[0], x[1])), + "ord_at[byte]": (x) => indexCall(func("bytes", x[0], text("u8")), x[1]), + "ord_at_back[byte]": (x) => + indexCall(func("bytes", x[0], text("u8")), x[1]), + "slice[codepoint]": (x) => + rangeIndexCall(x[0], x[1], op.add(x[1], x[2]), int(1)), + "slice[byte]": (x) => + method( + rangeIndexCall( + func("bytes", x[0], text("u8")), + x[1], + op.add(x[1], x[2]), + int(1), + ), + "decode", + text("u8"), + ), + "slice[List]": (x) => + rangeIndexCall(x[0], x[1], op.add(x[1], x[2]), int(1)), + split: (x) => method(x[0], "split", x[1]), + split_whitespace: (x) => method(x[0], "split"), - print: (x) => + "print[Text]": (x) => func( "print", x[0].kind !== "ImplicitConversion" ? [namedArg("end", x[0])] : [x[0], namedArg("end", text(""))], ), - text_replace: (x) => method(x[0], "replace", x[1], x[2]), + replace: (x) => method(x[0], "replace", x[1], x[2]), text_multireplace: (x) => method( @@ -164,22 +222,63 @@ const pythonLanguage: Language = { ), ), ), + + push: (x) => method(x[0], "append", x[1]), + append: (x) => op["concat[List]"](x[0], list([x[1]])), + right_align: (x) => + infix( + "%", + op["concat[Text]"](text("%"), op.int_to_dec(x[1]), text("s")), + x[0], + ), + int_to_bin: (x) => func("format", x[0], text("b")), + int_to_bin_aligned: (x) => + func( + "format", + x[0], + op["concat[Text]"](text("0"), op.int_to_dec(x[1]), text("b")), + ), + int_to_hex: (x) => infix("%", text("%X"), x[0]), + int_to_hex_aligned: (x) => + infix( + "%", + op["concat[Text]"](text("%0"), op.int_to_dec(x[1]), text("X")), + x[0], + ), + int_to_bool: (x) => implicitConversion("int_to_bool", x[0]), + bool_to_int: (x) => + op.mul(int(1n), implicitConversion("bool_to_int", x[0])), + include: (x) => method(x[0], "add", x[1]), + starts_with: (x) => method(x[0], "startsWith", x[1]), + ends_with: (x) => method(x[0], "endsWith", x[1]), }), mapTo(func)({ - read_line: "input", + "read[line]": "input", abs: "abs", - list_length: "len", - sorted: "sorted", - codepoint_to_int: "ord", - int_to_codepoint: "chr", + "size[List]": "len", + "size[Table]": "len", + "size[Set]": "len", + "sorted[Int]": "sorted", + "sorted[Ascii]": "sorted", + "ord[codepoint]": "ord", + "ord[byte]": "ord", + "char[codepoint]": "chr", + "char[byte]": "chr", max: "max", min: "min", - text_codepoint_length: "len", - int_to_text: "str", - text_to_int: "int", - println: "print", + "size[codepoint]": "len", + int_to_dec: "str", + dec_to_int: "int", + "println[Text]": "print", + gcd: "math.gcd", + }), + mapTo((x: string, [right, left]) => infix(x, left, right))({ + "contains[List]": "in", + "contains[Table]": "in", + "contains[Set]": "in", + "contains[Text]": "in", }), - mapToPrefixAndInfix( + mapUnaryAndBinary( { pow: "**", neg: "-", @@ -189,7 +288,8 @@ const pythonLanguage: Language = { div: "//", mod: "%", add: "+", - concat: "+", + "concat[Text]": "+", + "concat[List]": "+", sub: "-", bit_shift_left: "<<", bit_shift_right: ">>", @@ -198,8 +298,10 @@ const pythonLanguage: Language = { bit_or: "|", lt: "<", leq: "<=", - eq: "==", - neq: "!=", + "eq[Int]": "==", + "eq[Text]": "==", + "neq[Int]": "!=", + "neq[Text]": "!=", geq: ">=", gt: ">", not: "not", @@ -229,7 +331,11 @@ const pythonLanguage: Language = { ), required( renameIdents(), - addImports({ "sys.argv[1:]": "sys", "sys.argv": "sys" }), + addImports({ + "sys.argv[1:]": "sys", + "sys.argv": "sys", + "math.gcd": "math", + }), removeImplicitConversions, ), ], diff --git a/src/languages/python/plugins.ts b/src/languages/python/plugins.ts new file mode 100644 index 00000000..f494d997 --- /dev/null +++ b/src/languages/python/plugins.ts @@ -0,0 +1,55 @@ +import { getType } from "../../common/getType"; +import { + annotate, + builtin, + int, + isOp, + isText, + rangeIndexCall, + text, +} from "../../IR"; +import { type Plugin } from "../../common/Language"; +import { chars } from "../../common/strings"; + +export const golfTextListLiteralIndex: Plugin = { + name: "golfTextListLiteralIndex", + visit(node, spine) { + if ( + node.kind === "IndexCall" && + node.collection.kind === "List" && + node.collection.exprs.every(isText()) + ) { + const values = node.collection.exprs.map((x) => ({ + chars: chars(x.value), + targetLength: 0, + })); + // the length can never increase and it can only decrease by one + values.forEach((x, i) => { + x.targetLength = Math.max( + ...values.map((y, j) => y.chars.length - Number(j < i)), + ); + }); + if ( + values.every((x) => x.chars.length === x.targetLength) || + isOp("println[Text]")(spine.parent!.node) + ) { + values.forEach((x) => { + while (x.chars.length < x.targetLength) x.chars.push(" "); + }); + const combined = values[0].chars + .flatMap((_, i) => values.map((x) => x.chars[i])) + .filter((x) => x !== undefined) + .join(""); + return annotate( + rangeIndexCall( + text(combined), + node.index, + builtin(""), + int(values.length), + ), + getType(node, spine), + ); + } + } + }, +}; diff --git a/src/languages/python/python.test.md b/src/languages/python/python.test.md index ddd4cc53..338d95f8 100644 --- a/src/languages/python/python.test.md +++ b/src/languages/python/python.test.md @@ -20,14 +20,29 @@ print(4,end="") ```polygolf $a <- (text_get_codepoint "abcdefg" 4); -$b <- (text_get_codepoint_slice "abcdefg" 1 3); -$c <- (text_codepoint_reversed "abcdefg"); +$b <- (text_get_codepoint_slice "abcdefg" 2 3); +$c <- (slice_back[codepoint] "abcdefg" -4 3); +$d <- (slice_back[codepoint] "abcdefg" -4 4); +$e <- (text_codepoint_reversed "abcdefg"); +$f <- ("abcdefg" @ -2); ``` ```python nogolf a="abcdefg"[4] -b="abcdefg"[1:4] -c="abcdefg"[::-1] +b="abcdefg"[2:5] +c="abcdefg"[-4:-1] +d="abcdefg"[-4:] +e="abcdefg"[::-1] +f="abcdefg"[-2] +``` + +```polygolf +starts_with (@0) "abc"; +``` + +```py +import sys +sys.argv[1][:3]=="abc" ``` ## Text splitting @@ -132,7 +147,7 @@ for x in sys.argv[1:]:print(x) ``` ```polygolf -print (argv_get 0); +print (at[argv] 0); ``` ```python @@ -166,7 +181,7 @@ if ($a != 0) { $a <- 1; ``` -```python +```python no:hardcode a=1 if a:print(a) a=1 @@ -213,29 +228,21 @@ a=b="Hello" ## String encoding Ops ```polygolf -text_get_byte "abc" 1; -text_get_codepoint "def" 1; -text_byte_to_int "g"; -codepoint_to_int "h"; -text_get_byte_to_int "ijk" 1; -text_get_codepoint_to_int "lmn" 1; -text_byte_length "opq"; -text_codepoint_length "rst"; -int_to_text_byte 99; -int_to_codepoint 999; +$t <- "opq"; +at[Ascii] "abc" 1; +ord[Ascii] "g"; +ord_at[Ascii] "ijk" 1; +size[Ascii] $t; +char[Ascii] 99; ``` ```py nogolf +t="opq" "abc"[1] -"def"[1] ord("g") -ord("h") ord("ijk"[1]) -ord("lmn"[1]) -len("opq") -len("rst") +len(t) chr(99) -chr(999) ``` ## Aliasing partially applied methods @@ -296,3 +303,37 @@ for _ in"X"*10:print("Hi") -int('7m93qx4grzs1ls98c9nh5rs313rz0u',36) 7*10**22-1 ``` + +## Golfing literal text list access + +```polygolf +list_get (list "false" "true") 1; +``` + +```py +"ftarlusee"[1::2] +``` + +```polygolf +list_get (list "12345" "ABC") 1; +println (list_get (list "12345" "ABC") 1); +``` + +```py simple +["12345","ABC"][1] +print("1A2B3C4 5"[1::2]) +``` + +## Conditional ops + +```polygolf +conditional true 3 4; +``` + +```python nogolf +3 if 1 else 4 +``` + +```python +[4,3][1] +``` diff --git a/src/languages/swift/emit.ts b/src/languages/swift/emit.ts index 612ee0b0..70af6ccf 100644 --- a/src/languages/swift/emit.ts +++ b/src/languages/swift/emit.ts @@ -5,7 +5,7 @@ import { emitTextFactory, joinTrees, } from "../../common/emit"; -import { type IR, isIntLiteral } from "../../IR"; +import { type IR, isInt } from "../../IR"; import { type CompilationContext } from "@/common/compile"; const unicode01to09repls = { @@ -68,6 +68,8 @@ function precedence(expr: IR.Node): number { return unaryPrecedence(expr.name); case "Infix": return binaryPrecedence(expr.name); + case "ConditionalOp": + return 0; } return Infinity; } @@ -158,7 +160,7 @@ export default function emitProgram( "for", e.variable === undefined ? "_" : emit(e.variable), "in", - isIntLiteral(1n)(e.increment) + isInt(1n)(e.increment) ? [start, e.inclusive ? "..." : "..<", end] : [ "stride", @@ -202,23 +204,23 @@ export default function emitProgram( case "FunctionCall": return [emit(e.func), "(", joinNodes(",", e.args), ")"]; case "PropertyCall": - return [emit(e.object), ".", e.ident.name]; + return [emit(e.object, Infinity), ".", e.ident.name]; case "MethodCall": return [ - emit(e.object), + emit(e.object, Infinity), ".", e.ident.name, "(", - joinNodes(", ", e.args), + joinNodes(",", e.args), ")", ]; case "ConditionalOp": return [ - emit(e.condition), + emit(e.condition, prec + 1), "?", emit(e.consequent), ":", - emit(e.alternate), + emit(e.alternate, prec), ]; case "Infix": { return [emit(e.left, prec), e.name, emit(e.right, prec + 1)]; @@ -229,6 +231,8 @@ export default function emitProgram( return [emit(e.arg, prec), e.name]; case "List": return ["[", joinNodes(",", e.exprs), "]"]; + case "Set": + return ["Set([", joinNodes(",", e.exprs), "])"]; case "Table": return [ "[", @@ -246,6 +250,15 @@ export default function emitProgram( "]", e.collection.kind === "Table" ? "!" : "", ]; + case "RangeIndexCall": + return [ + emit(e.collection, Infinity), + "[", + emit(e.low), + "..<", + emit(e.high), + "]", + ]; default: throw new EmitError(expr); diff --git a/src/languages/swift/index.ts b/src/languages/swift/index.ts index dd39e8bd..84d8b1c0 100644 --- a/src/languages/swift/index.ts +++ b/src/languages/swift/index.ts @@ -5,12 +5,16 @@ import { namedArg, op, text, - add1, + succ, propertyCall as prop, isText, builtin, int, postfix, + isInt, + list, + conditional, + rangeIndexCall, } from "../../IR"; import { type Language, @@ -24,15 +28,26 @@ import { import emitProgram from "./emit"; import { mapOps, - mapToPrefixAndInfix, + mapUnaryAndBinary, useIndexCalls, flipBinaryOps, removeImplicitConversions, printIntToPrint, + arraysToLists, + backwardsIndexToForwards, } from "../../plugins/ops"; import { alias, renameIdents } from "../../plugins/idents"; -import { golfStringListLiteral, listOpsToTextOps } from "../../plugins/static"; -import { golfLastPrint, implicitlyConvertPrintArg } from "../../plugins/print"; +import { + golfStringListLiteral, + hardcode, + listOpsToTextOps, +} from "../../plugins/static"; +import { + golfLastPrint, + implicitlyConvertPrintArg, + putcToPrintChar, + mergePrint, +} from "../../plugins/print"; import { assertInt64 } from "../../plugins/types"; import { addVarDeclarations, @@ -46,7 +61,7 @@ import { forRangeToForRangeOneStep, } from "../../plugins/loops"; import { - useEquivalentTextOp, + usePrimaryTextOps, textToIntToTextGetToInt, replaceToSplitAndJoin, } from "../../plugins/textOps"; @@ -66,19 +81,20 @@ const swiftLanguage: Language = { extension: "swift", emitter: emitProgram, phases: [ - required(printIntToPrint), + search(hardcode()), + required(printIntToPrint, arraysToLists, usePrimaryTextOps("codepoint")), + simplegolf(golfLastPrint()), search( + mergePrint, flipBinaryOps, golfStringListLiteral(false), listOpsToTextOps(), - golfLastPrint(), equalityToInequality, forRangeToForRangeInclusive(), ...bitnotPlugins, ...lowBitsPlugins, applyDeMorgans, forRangeToForRangeOneStep, - useEquivalentTextOp(true, true), inlineVariables, replaceToSplitAndJoin, textToIntToTextGetToInt, @@ -86,31 +102,31 @@ const swiftLanguage: Language = { ...truncatingOpsPlugins, mapOps({ argv: builtin("CommandLine.arguments[1...]"), - argv_get: (x) => - op("list_get", builtin("CommandLine.arguments"), add1(x[0])), - codepoint_to_int: (x) => op("text_get_codepoint_to_int", x[0], int(0n)), - text_byte_to_int: (x) => op("text_get_byte_to_int", x[0], int(0n)), - text_get_byte: (x) => - op("int_to_text_byte", op("text_get_byte_to_int", ...x)), + "at[argv]": (x) => + op["at[List]"](builtin("CommandLine.arguments"), succ(x[0])), + "ord[codepoint]": (x) => op["ord_at[codepoint]"](x[0], int(0n)), + "ord[byte]": (x) => op["ord_at[byte]"](x[0], int(0n)), + "at[byte]": (x) => op["char[byte]"](op["ord_at[byte]"](x[0], x[1])), }), - useIndexCalls(), + decomposeIntLiteral(), ), required( + backwardsIndexToForwards(), + useIndexCalls(), + putcToPrintChar, pickAnyInt, forArgvToForEach, ...truncatingOpsPlugins, mapOps({ - read_line: func("readLine"), + "read[line]": func("readLine"), argv: builtin("CommandLine.arguments[1...]"), - argv_get: (x) => - op("list_get", builtin("CommandLine.arguments"), add1(x[0])), - codepoint_to_int: (x) => op("text_get_codepoint_to_int", x[0], int(0n)), - text_byte_to_int: (x) => op("text_get_byte_to_int", x[0], int(0n)), - text_get_byte: (x) => - op("int_to_text_byte", op("text_get_byte_to_int", ...x)), + "at[argv]": (x) => + op["at[List]"](builtin("CommandLine.arguments"), succ(x[0])), + "ord[codepoint]": (x) => op["ord_at[codepoint]"](x[0], int(0n)), + "ord[byte]": (x) => op["ord_at[byte]"](x[0], int(0n)), + "at[byte]": (x) => op["char[byte]"](op["ord_at[byte]"](x[0], x[1])), }), - useIndexCalls(), implicitlyConvertPrintArg, mapOps({ join: (x) => @@ -119,47 +135,125 @@ const swiftLanguage: Language = { "joined", ...(isText("")(x[1]) ? [] : [namedArg("separator", x[1])]), ), - text_get_byte_to_int: (x) => + "ord_at[byte]": (x) => func("Int", indexCall(func("Array", prop(x[0], "utf8")), x[1])), - text_get_codepoint: (x) => + "at[codepoint]": (x) => func("String", indexCall(func("Array", x[0]), x[1])), - text_get_codepoint_to_int: (x) => + "slice[codepoint]": (x) => + isInt(0n)(x[1]) + ? method(x[0], "prefix", x[2]) + : method( + method(x[0], "prefix", op.add(x[1], x[2])), + "suffix", + x[2], + ), + "slice[List]": (x) => + rangeIndexCall(x[0], x[1], op.add(x[1], x[2]), int(1n)), + "ord_at[codepoint]": (x) => prop( indexCall(func("Array", prop(x[0], "unicodeScalars")), x[1]), "value", ), - int_to_text_byte: (x) => + "char[byte]": (x) => func("String", postfix("!", func("UnicodeScalar", x))), - int_to_codepoint: (x) => + "char[codepoint]": (x) => func("String", postfix("!", func("UnicodeScalar", x))), - text_codepoint_length: (x) => prop(x[0], "count"), - text_byte_length: (x) => prop(prop(x[0], "utf8"), "count"), - int_to_text: (x) => func("String", x), - text_split: (x) => method(x[0], "split", namedArg("separator", x[1])), + "size[codepoint]": (x) => prop(x[0], "count"), + "size[byte]": (x) => prop(prop(x[0], "utf8"), "count"), + "size[List]": (x) => prop(x[0], "count"), + "size[Set]": (x) => prop(x[0], "count"), + "size[Table]": (x) => prop(x[0], "count"), + "reversed[codepoint]": (x) => func("String", method(x[0], "reversed")), + "reversed[List]": (x) => func("Array", method(x[0], "reversed")), + "sorted[Int]": (x) => method(x[0], "sorted"), + "sorted[Ascii]": (x) => method(x[0], "sorted"), + int_to_dec: (x) => func("String", x), + split: (x) => + method( + x[0], + "split", + namedArg("separator", x[1]), + namedArg("omittingEmptySubsequences", op.false), + ), repeat: (x) => func("String", namedArg("repeating", x[0]), namedArg("count", x[1])), - + "contains[Text]": (x) => method(x[0], "contains", x[1]), + "contains[List]": (x) => method(x[0], "contains", x[1]), + "contains[Set]": (x) => method(x[0], "contains", x[1]), + "contains[Table]": (x) => method(prop(x[0], "keys"), "contains", x[1]), + "find[List]": (x) => method(x[0], "index", namedArg("of", x[1])), + "find[codepoint]": (x) => + conditional( + op["contains[Text]"](x[0], x[1]), + op["size[codepoint]"]( + op["at[List]"](op.split(x[0], x[1]), int(0n)), + ), + int(-1n), + ), + "find[byte]": (x) => + conditional( + op["contains[Text]"](x[0], x[1]), + op["size[byte]"](op["at[List]"](op.split(x[0], x[1]), int(0n))), + int(-1n), + ), pow: (x) => func("Int", func("pow", func("Double", x[0]), func("Double", x[1]))), - println: (x) => func("print", x), - print: (x) => func("print", x, namedArg("terminator", text(""))), - text_to_int: (x) => postfix("!", func("Int", x)), + "println[Text]": (x) => func("print", x), + "print[Text]": (x) => + func("print", x, namedArg("terminator", text(""))), + dec_to_int: (x) => postfix("!", func("Int", x)), + append: (x) => op["concat[List]"](x[0], list([x[1]])), + include: (x) => method(x[0], "insert", x[1]), + push: (x) => method(x[0], "append", x[1]), max: (x) => func("max", x), min: (x) => func("min", x), abs: (x) => func("abs", x), true: builtin("true"), false: builtin("false"), + bool_to_int: (x) => conditional(x[0], int(1n), int(0n)), + int_to_bool: (x) => op["neq[Int]"](x[0], int(0n)), + int_to_hex: (x) => + func( + "String", + x[0], + namedArg("radix", int(16n)), + namedArg("uppercase", op.true), + ), + int_to_bin: (x) => func("String", x[0], namedArg("radix", int(2n))), + int_to_hex_aligned: (x) => + func( + "String", + namedArg( + "format", + op["concat[Text]"](text("%0"), op.int_to_dec(x[1]), text("X")), + ), + x[0], + ), + int_to_bin_aligned: (x) => + method( + op["concat[Text]"](op.repeat(text("0"), x[1]), op.int_to_bin(x[0])), + "suffix", + x[1], + ), + right_align: (x) => + method( + op["concat[Text]"](op.repeat(text(" "), x[1]), x[0]), + "suffix", + x[1], + ), - text_replace: (x) => + replace: (x) => method( x[0], "replacingOccurrences", namedArg("of", x[1]), namedArg("with", x[2]), ), + starts_with: (x) => method(x[0], "hasPrefix", x[1]), + ends_with: (x) => method(x[0], "hasSuffix", x[1]), }), - mapToPrefixAndInfix( + mapUnaryAndBinary( { not: "!", neg: "-", @@ -174,11 +268,14 @@ const swiftLanguage: Language = { sub: "-", bit_or: "|", bit_xor: "^", - concat: "+", + "concat[Text]": "+", + "concat[List]": "+", lt: "<", leq: "<=", - eq: "==", - neq: "!=", + "eq[Int]": "==", + "eq[Text]": "==", + "neq[Int]": "!=", + "neq[Text]": "!=", geq: ">=", gt: ">", and: "&&", @@ -186,7 +283,12 @@ const swiftLanguage: Language = { }, ["+", "-", "*", "/", "%", "&", "|", "^", "<<", ">>"], ), - addImports({ pow: "Foundation", replacingOccurrences: "Foundation" }), + useIndexCalls(), + addImports({ + pow: "Foundation", + replacingOccurrences: "Foundation", + format: "Foundation", + }), ), simplegolf( alias({ @@ -218,7 +320,7 @@ const swiftLanguage: Language = { nextToken: string, ): boolean { return ( - (/^[-+*/<>=^*|~]+$/.test(token) && /[-~]/.test(nextToken[0])) || + (/^[-+*%/<>=^*|~]+$/.test(token) && /[-~]/.test(nextToken[0])) || (token === `&` && /[*+-]/.test(nextToken[0])) || token === `!=` ); diff --git a/src/languages/swift/swift.test.md b/src/languages/swift/swift.test.md index 821c7156..62f00bce 100644 --- a/src/languages/swift/swift.test.md +++ b/src/languages/swift/swift.test.md @@ -60,8 +60,9 @@ vwx ```polygolf $a:-100..100 <- 0; -$b:Text <- "xy"; +$b <- "xy"; $c <- (0==0); +$d <- (list "xy" "abc" "123"); % Boolean and $c $c; @@ -107,33 +108,54 @@ $a <- ($a | 2):-100..100; $a <- ($a ~ 2):-100..100; % Text encoding -text_get_byte "abc" 1; -text_get_codepoint "abc" 1; -text_get_byte_to_int "abc" 1; -text_get_codepoint_to_int "abc" 1; -text_byte_length "abc"; -text_codepoint_length "abc"; -text_byte_to_int "a"; -codepoint_to_int "\u00ff"; -int_to_text_byte 99; -int_to_codepoint 999; +at[byte] $b 1; +at[codepoint] $b 1; +ord_at[byte] $b 1; +ord_at[codepoint] $b 1; +size[byte] $b; +size[codepoint] $b; +ord[byte] "a"; +ord[codepoint] "\u00ff"; +char[byte] 99; +char[codepoint] 999; +slice[codepoint] $b 2 3; % Other -list_get (list "xy" "abc") 1; -concat $b "xyz"; -int_to_text 5; -text_to_int "5"; +at[List] $d 1; +concat[Text] $b "xyz"; +concat[List] $d $d; +reversed[codepoint] $b; +reversed[List] $d; +sorted[Ascii] $d; +sorted[Int] (list 4 3 1 2); +size[Set] (set 1 2); +size[Table] (table ("X" => "Y") ); +size[List] $d; +contains[Text] $b "b"; +contains[List] $d "123"; +contains[Set] (set 1 2) 2; +contains[Table] (table ("X" => "Y") ) "X"; +find[List] $d "xy"; +find[codepoint] "abcdef" "de"; +find[byte] "abcdef" "de"; +int_to_dec 5; +dec_to_int "5"; text_split "xyz" "y"; -join (list "xy" "abc") "/"; -join (list "12" "345") ""; +join $d "/"; +join $d ""; repeat $b 3; text_replace "a+b+c" "+" "*"; -table_get (table ("X" => "Y") ) "X"; +at[Table] (table ("X" => "Y") ) "X"; +int_to_hex_aligned 50 7; +int_to_hex 50; +int_to_bin_aligned 50 7; +int_to_bin 50; +right_align "text" 20; ``` ```swift nogolf import Foundation -var a=0,b="xy",c=0==0 +var a=0,b="xy",c=0==0,d=["xy","abc","123"] c&&c c||c !c @@ -167,26 +189,47 @@ a%=2 a&=2 a|=2 a^=2 -String(UnicodeScalar(Int(Array("abc".utf8)[1]))!) -String(Array("abc")[1]) -Int(Array("abc".utf8)[1]) -Array("abc".unicodeScalars)[1].value -"abc".utf8.count -"abc".count +String(UnicodeScalar(Int(Array(b.utf8)[1]))!) +String(Array(b)[1]) +Int(Array(b.utf8)[1]) +Array(b.unicodeScalars)[1].value +b.utf8.count +b.count Int(Array("a".utf8)[0]) Array("ÿ".unicodeScalars)[0].value String(UnicodeScalar(99)!) String(UnicodeScalar(999)!) -["xy","abc"][1] +b.prefix(5).suffix(3) +d[1] b+"xyz" +d+d +String(b.reversed()) +Array(d.reversed()) +d.sorted() +[4,3,1,2].sorted() +Set([1,2]).count +["X":"Y"].count +d.count +b.contains("b") +d.contains("123") +Set([1,2]).contains(2) +["X":"Y"].keys.contains("X") +d.index(of:"xy") +"abcdef".contains("de") ?"abcdef".split(separator:"de",omittingEmptySubsequences:false)[0].count:-1 +"abcdef".contains("de") ?"abcdef".split(separator:"de",omittingEmptySubsequences:false)[0].utf8.count:-1 String(5) Int("5")! -"xyz".split(separator:"y") -["xy","abc"].joined(separator:"/") -["12","345"].joined() +"xyz".split(separator:"y",omittingEmptySubsequences:false) +d.joined(separator:"/") +d.joined() String(repeating:b,count:3) -"a+b+c".replacingOccurrences(of:"+", with:"*") +"a+b+c".replacingOccurrences(of:"+",with:"*") ["X":"Y"]["X"]! +String(format:"%0"+String(7)+"X",50) +String(50,radix:16,uppercase:true) +(String(repeating:"0",count:7)+String(50,radix:2)).suffix(7) +String(50,radix:2) +(String(repeating:" ",count:20)+"text").suffix(20) ``` ## Whitespace behavior @@ -228,7 +271,7 @@ for x in CommandLine.arguments[1...]{print(x)} ``` ```polygolf -argv_get 0; +at[argv] 0; ``` ```swift nogolf @@ -250,3 +293,17 @@ CommandLine.arguments[1] "\u{161}" "\u{1f48e}" ``` + +## Conditional + +```polygolf +$a:Int <- 1; +$b:Int <- 1; +$c:Int <- 1; +println_int (conditional (conditional ($a > 0) ($b > 0) ($c > 0)) 8 7); +``` + +```swift nogolf +var a=1,b=1,c=1 +print((a>0 ?b>0:c>0) ?8:7) +``` diff --git a/src/languages/tex/FlatIR.ts b/src/languages/tex/FlatIR.ts index 29ebf070..30421994 100644 --- a/src/languages/tex/FlatIR.ts +++ b/src/languages/tex/FlatIR.ts @@ -76,7 +76,7 @@ class FlatIRChunk { const arg = this.addNode(node.args[0], true); if (arg === null) throw new EmitError(node.args[0], "Unary Op arg is void"); - const opres = op(node.op, arg); + const opres = op.unsafe(node.op, arg); // An op like println_int which returns void. this.pushInstruction(opres); return null; @@ -90,7 +90,7 @@ class FlatIRChunk { throw new EmitError(cond, "flattening if condition"); const left = this.addNodeRequired(cond.args[0], true); const right = this.addNodeRequired(cond.args[1], true); - const newCond = op(cond.op, left, right); + const newCond = op.unsafe(cond.op, left, right); const stmt = ifStatement(newCond, node.consequent, node.alternate); this.pushInstruction(stmt); return null; diff --git a/src/languages/tex/index.ts b/src/languages/tex/index.ts index 70215a89..98265a57 100644 --- a/src/languages/tex/index.ts +++ b/src/languages/tex/index.ts @@ -1,5 +1,5 @@ import emitProgram from "./emit"; -import { mapToPrefixAndInfix } from "../../plugins/ops"; +import { mapUnaryAndBinary } from "../../plugins/ops"; import { forRangeToWhile, whileToRecursion } from "../../plugins/loops"; import { lettersOnlyIdentGen, renameIdents } from "../../plugins/idents"; import { type Language, required } from "../../common/Language"; @@ -21,7 +21,7 @@ const texLanguage: Language = { required( forRangeToWhile, whileToRecursion, - mapToPrefixAndInfix( + mapUnaryAndBinary( { mul: "\\multiply", /** helper_mod is defined in addTeXImports */ @@ -41,6 +41,7 @@ const texLanguage: Language = { preferred: (o) => lettersOnlyIdentGen.preferred(o).map((w) => "\\" + w), short: ["~"].concat(lettersOnlyIdentGen.short.map((c) => "\\" + c)), general: (i) => "\\" + lettersOnlyIdentGen.general(i), + reserved: [], }), macroParamsToHash, ), diff --git a/src/languages/tex/plugins.ts b/src/languages/tex/plugins.ts index 96c1adff..6f9e6e73 100644 --- a/src/languages/tex/plugins.ts +++ b/src/languages/tex/plugins.ts @@ -30,12 +30,10 @@ import { addDefinitions } from "../../plugins/imports"; // TODO: I don't know what's the actual term. 3 argument code? export const exprTreeToFlat2AC: Plugin = { name: "exprTreeToFlat2AC", - visit(_node, spine) { - return spine.flatMapWithChildrenReplacer((node, spine) => { - if (spine.parent?.node.kind !== "Block") return; - if (isOfKind("Assignment", "Op", "If")(node)) - return convertNodeToListOfStatements(node); - }); + visit(node, spine) { + if (spine.parent?.node.kind !== "Block") return; + if (isOfKind("Assignment", "Op", "If")(node)) + return [...convertNodeToListOfStatements(node)]; }, }; @@ -58,9 +56,9 @@ export const stuffToMacros: Plugin = { return ifToMacros(node, spine); case "Op": switch (node.op) { - case "println_int": { - const arg = node.args[0]; - assertImmediate(arg, "println_int"); + case "println[Int]": { + const arg = node.args[0]!; + assertImmediate(arg, "println[Int]"); return voidIt( // TODO: \\endgraf is long but works everywhere. Try \n\n sometimes // TODO: the \\endgraf should be outside the other scanningMacroCall. diff --git a/src/languages/text/index.ts b/src/languages/text/index.ts new file mode 100644 index 00000000..8273c96a --- /dev/null +++ b/src/languages/text/index.ts @@ -0,0 +1,11 @@ +import { getOutput } from "../../interpreter"; +import type { Language } from "../../common/Language"; + +const textLanguage: Language = { + name: "Text", + extension: "txt", + phases: [], + emitter: getOutput, +}; + +export default textLanguage; diff --git a/src/languages/text/text.test.md b/src/languages/text/text.test.md new file mode 100644 index 00000000..4004ce4b --- /dev/null +++ b/src/languages/text/text.test.md @@ -0,0 +1,37 @@ +# Text + +```polygolf +% Printing +println "Hello, World!"; + +% Looping +for $i 0 10 { + println_int $i; +}; +``` + +```txt +Hello, World! +0 +1 +2 +3 +4 +5 +6 +7 +8 +9 +``` + +## Declaration needs block + +```polygolf +for $i 0 32 { + $t <- 0; +}; +``` + +```text + +``` diff --git a/src/markdown-tests/index.ts b/src/markdown-tests/index.ts index 91c2fa6c..f8bed7ae 100644 --- a/src/markdown-tests/index.ts +++ b/src/markdown-tests/index.ts @@ -4,10 +4,12 @@ import compile, { applyAllToAllAndGetCounts, debugEmit, normalize, + typecheck, } from "../common/compile"; import { findLang } from "../languages/languages"; import { type Plugin } from "../common/Language"; import { getOnlyVariant } from "../common/expandVariants"; +import type { PluginVisitor } from "../common/Spine"; export const keywords = [ "nogolf", @@ -21,6 +23,8 @@ export const keywords = [ "restrictFrontend", "1..127", "32..127", + "no:hardcode", + "noEmit", ] as const; export function compilationOptionsFromKeywords( @@ -39,6 +43,8 @@ export function compilationOptionsFromKeywords( getAllVariants: is("allVariants"), restrictFrontend: is("restrictFrontend"), skipTypecheck: isLangTest ? is("skipTypecheck") : !is("typecheck"), + skipPlugins: is("no:hardcode") ? ["hardcode"] : [], + noEmit: is("noEmit"), }; } @@ -62,23 +68,28 @@ export function testLang( export function testPlugin( name: string, - plugins: Plugin[], + plugins: (Plugin | PluginVisitor)[], args: string[], input: string, output: string, ) { test(name, () => { expect( - debugEmit( - applyAllToAllAndGetCounts( - getOnlyVariant(parse(input, false)), - { - addWarning: () => {}, - options: compilationOptionsFromKeywords(args), - }, - ...plugins.map((x) => x.visit), - )[0], - ), + (() => { + const options = compilationOptionsFromKeywords(args, false); + let program = getOnlyVariant(parse(input, false).node); + program = typecheck(program, !options.skipTypecheck); + return debugEmit( + applyAllToAllAndGetCounts( + program, + options, + () => {}, + ...plugins.map((x) => + typeof x === "function" ? { name: x.name, visit: x } : x, + ), + )[0], + ); + })(), ).toEqual(normalize(output)); }); } diff --git a/src/plugins/arithmetic.ts b/src/plugins/arithmetic.ts index 9f6cf3fb..a9fc6006 100644 --- a/src/plugins/arithmetic.ts +++ b/src/plugins/arithmetic.ts @@ -8,154 +8,142 @@ import { type Op, isSubtype, isOp, - isIntLiteral, + isInt, implicitConversion, integerType, type Node, - sub1, - add1, + prec, + succ, } from "../IR"; import { getType } from "../common/getType"; import { mapOps } from "./ops"; import { type Spine } from "@/common/Spine"; import { filterInplace } from "../common/arrays"; -export const modToRem: Plugin = { - name: "modToRem", - visit(node, spine) { - if (isOp("mod")(node)) { - return isSubtype(getType(node.args[1], spine), integerType(0)) - ? op("rem", ...node.args) - : op( - "rem", - op("add", op("rem", ...node.args), node.args[1]), - node.args[1], - ); - } - }, -}; - -export const divToTruncdiv: Plugin = { - name: "divToTruncdiv", - visit(node, spine) { - if (isOp("div")(node)) { - return isSubtype(getType(node.args[1], spine), integerType(0)) - ? op("trunc_div", ...node.args) - : undefined; // TODO - } - }, -}; +export function modToRem(node: Node, spine: Spine) { + if (isOp("mod")(node)) { + return isSubtype(getType(node.args[1], spine), integerType(0)) + ? op.rem(...node.args) + : op.rem(op.add(op.rem(...node.args), node.args[1]), node.args[1]); + } +} + +export function divToTruncdiv(node: Node, spine: Spine) { + if (isOp("div")(node)) { + return isSubtype(getType(node.args[1], spine), integerType(0)) + ? op.trunc_div(...node.args) + : undefined; // TODO + } +} export const truncatingOpsPlugins = [modToRem, divToTruncdiv]; -export const equalityToInequality: Plugin = { - name: "equalityToInequality", - visit(node, spine) { - if (isOp("eq", "neq")(node)) { - const eq = node.op === "eq"; - const [a, b] = [node.args[0], node.args[1]]; - const [t1, t2] = [a, b].map((x) => getType(x, spine)) as [ - IntegerType, - IntegerType, - ]; - if (isConstantType(t1)) { - if (t1.low === t2.low) { - // (0 == $x:0..9) -> (1 > $x:0..9) - // (0 != $x:0..9) -> (0 < $x:0..9) - return eq ? op("gt", int(t1.low + 1n), b) : op("lt", int(t1.low), b); - } - if (t1.low === t2.high) { - // (9 == $x:0..9) -> (8 < $x:0..9) - // (9 != $x:0..9) -> (9 > $x:0..9) - return eq ? op("lt", int(t1.low - 1n), b) : op("gt", int(t1.low), b); - } +export function equalityToInequality(node: Node, spine: Spine) { + if (isOp("eq[Int]", "neq[Int]")(node)) { + const eq = node.op === "eq[Int]"; + const [a, b] = [node.args[0], node.args[1]]; + const [t1, t2] = [a, b].map((x) => getType(x, spine)) as [ + IntegerType, + IntegerType, + ]; + if (isConstantType(t1)) { + if (t1.low === t2.low) { + // (0 == $x:0..9) -> (1 > $x:0..9) + // (0 != $x:0..9) -> (0 < $x:0..9) + return eq ? op.gt(int(t1.low + 1n), b) : op.lt(int(t1.low), b); } + if (t1.low === t2.high) { + // (9 == $x:0..9) -> (8 < $x:0..9) + // (9 != $x:0..9) -> (9 > $x:0..9) + return eq ? op.lt(int(t1.low - 1n), b) : op.gt(int(t1.low), b); + } + } - if (isConstantType(t2)) { - if (t1.low === t2.low) { - // ($x:0..9 == 0) -> ($x:0..9 < 1) - // ($x:0..9 != 0) -> ($x:0..9 > 0) - return eq ? op("lt", a, int(t2.low + 1n)) : op("gt", a, int(t2.low)); - } - if (t1.high === t2.low) { - // ($x:0..9 == 9) -> ($x:0..9 > 8) - // ($x:0..9 != 9) -> ($x:0..9 < 9) - return eq ? op("gt", a, int(t2.low - 1n)) : op("lt", a, int(t2.low)); - } + if (isConstantType(t2)) { + if (t1.low === t2.low) { + // ($x:0..9 == 0) -> ($x:0..9 < 1) + // ($x:0..9 != 0) -> ($x:0..9 > 0) + return eq ? op.lt(a, int(t2.low + 1n)) : op.gt(a, int(t2.low)); + } + if (t1.high === t2.low) { + // ($x:0..9 == 9) -> ($x:0..9 > 8) + // ($x:0..9 != 9) -> ($x:0..9 < 9) + return eq ? op.gt(a, int(t2.low - 1n)) : op.lt(a, int(t2.low)); } } - }, -}; + } +} export const removeBitnot: Plugin = mapOps( - { bit_not: (x) => op("sub", int(-1), x[0]) }, + { bit_not: (x) => op.sub(int(-1), x[0]) }, "removeBitnot", ); -export const addBitnot: Plugin = { - name: "addBitnot", - visit(node) { - if ( - isOp("add")(node) && - node.args.length === 2 && - isIntLiteral()(node.args[0]) - ) { - if (node.args[0].value === 1n) - return op("neg", op("bit_not", node.args[1])); - if (node.args[0].value === -1n) - return op("bit_not", op("neg", node.args[1])); - } - }, -}; +export function addBitnot(node: Node) { + if (isOp("add")(node) && node.args.length === 2 && isInt()(node.args[0])) { + if (node.args[0].value === 1n) return op.neg(op.bit_not(node.args[1])); + if (node.args[0].value === -1n) return op.bit_not(op.neg(node.args[1])); + } +} export const bitnotPlugins = [removeBitnot, addBitnot]; -export const applyDeMorgans: Plugin = { - name: "applyDeMorgans", - visit(node, spine) { - if (isOp("and", "or", "unsafe_and", "unsafe_or")(node)) { - const negation = op( - node.op === "and" - ? "or" - : node.op === "or" - ? "and" - : node.op === "unsafe_and" - ? "unsafe_or" - : "unsafe_and", - ...node.args.map((x) => op("not", x)), - ); - if (getType(node, spine).kind === "void") return negation; // If we are promised we won't read the result, we don't need to negate. - return op("not", negation); - } - if (isOp("bit_and", "bit_or")(node)) { - return op( - "bit_not", - op( - node.op === "bit_and" ? "bit_or" : "bit_and", - ...node.args.map((x) => op("bit_not", x)), - ), - ); - } - }, -}; +type BinaryBoolOp = "and" | "or" | "unsafe_and" | "unsafe_or"; +function complementaryBoolOp(op: BinaryBoolOp): BinaryBoolOp { + switch (op) { + case "and": + return "or"; + case "or": + return "and"; + case "unsafe_and": + return "unsafe_or"; + case "unsafe_or": + return "unsafe_and"; + } +} -export const useIntegerTruthiness: Plugin = { - name: "useIntegerTruthiness", - visit(node, spine) { +export function applyDeMorgans(node: Node, spine: Spine) { + if (isOp("and", "or", "unsafe_and", "unsafe_or")(node)) { if ( - isOp("eq", "neq")(node) && - spine.parent!.node.kind === "If" && - spine.pathFragment === "condition" + isOp("unsafe_and", "unsafe_or")(node) && + getType(node, spine).kind === "void" ) { - const res = isIntLiteral(0n)(node.args[1]) - ? implicitConversion("int_to_bool", node.args[0]) - : isIntLiteral(0n)(node.args[0]) - ? implicitConversion("int_to_bool", node.args[1]) - : undefined; - return res !== undefined && node.op === "eq" ? op("not", res) : res; + // If we are promised we won't read the result, we don't need to negate. + return op.unsafe( + complementaryBoolOp(node.op), + op.not(node.args[0]), + node.args[1], + ); } - }, -}; + return op.not( + op.unsafe(complementaryBoolOp(node.op), ...node.args.map(op.not)), + ); + } + if (isOp("bit_and", "bit_or")(node)) { + return op.bit_not( + op.unsafe( + node.op === "bit_and" ? "bit_or" : "bit_and", + ...(node.args.map(op.bit_not) as any), + ), + ); + } +} + +export function useIntegerTruthiness(node: Node, spine: Spine) { + if ( + isOp("eq[Int]", "neq[Int]")(node) && + !spine.isRoot && + spine.parent!.node.kind === "If" && + spine.pathFragment === "condition" + ) { + const res = isInt(0n)(node.args[1]) + ? implicitConversion("int_to_bool", node.args[0]) + : isInt(0n)(node.args[0]) + ? implicitConversion("int_to_bool", node.args[1]) + : undefined; + return res !== undefined && node.op === "eq[Int]" ? op.not(res) : res; + } +} function isConstantTypePowerOfTwo(n: Node, s: Spine) { const type = getType(n, s); @@ -173,31 +161,25 @@ function isPowerOfTwo(n: Node, s: Spine): boolean { ); } -export const modToBitand: Plugin = { - name: "modToBitand", - visit(node, spine) { - if (isOp("mod")(node)) { - const n = node.args[1]; - if (isPowerOfTwo(n, spine)) { - return op("bit_and", node.args[0], sub1(n)); - } +export function modToBitand(node: Node, spine: Spine) { + if (isOp("mod")(node)) { + const n = node.args[1]; + if (isPowerOfTwo(n, spine)) { + return op.bit_and(node.args[0], prec(n)); } - }, -}; - -export const bitandToMod: Plugin = { - name: "bitandToMod", - visit(node, spine) { - if (isOp("bit_and")(node)) { - for (const i of [0, 1]) { - const n = add1(node.args[i]); - if (isPowerOfTwo(n, spine)) { - return op("mod", node.args[1 - i], n); - } + } +} + +export function bitandToMod(node: Node, spine: Spine) { + if (isOp("bit_and")(node)) { + for (const i of [0, 1]) { + const n = succ(node.args[i]); + if (isPowerOfTwo(n, spine)) { + return op.mod(node.args[1 - i], n); } } - }, -}; + } +} export const lowBitsPlugins = [modToBitand, bitandToMod]; @@ -207,38 +189,35 @@ export function powToMul(limit: number = 2): Plugin { visit(node) { if (isOp("pow")(node)) { const [a, b] = node.args; - if (isIntLiteral()(b) && 1 < b.value && b.value <= limit) { - return op("mul", ...Array(Number(b.value)).fill(a)); + if (isInt()(b) && 1 < b.value && b.value <= limit) { + return op.mul(a, a, ...Array(Number(b.value) - 2).fill(a)); } } }, }; } -export const mulToPow: Plugin = { - name: "mulToPow", - visit(node) { - if (isOp("mul")(node)) { - const factors = new Map(); - for (const e of node.args) { - const stringified = stringify(e); - factors.set(stringified, [ - e, - 1 + ((factors.get(stringified)?.at(1) as number) ?? 0), - ]); - } - const pairs = [...factors.values()]; - if (pairs.some((pair) => pair[1] > 1)) { - return op( - "mul", - ...pairs.map(([expr, exp]) => - exp > 1 ? op("pow", expr, int(exp)) : expr, - ), - ); - } +export function mulToPow(node: Node, spine: Spine) { + if (isOp("mul")(node)) { + const factors = new Map(); + for (const e of node.args) { + const stringified = stringify(e); + factors.set(stringified, [ + e, + 1 + ((factors.get(stringified)?.at(1) as number) ?? 0), + ]); } - }, -}; + const pairs = [...factors.values()]; + if (pairs.some((pair) => pair[1] > 1)) { + return op.unsafe( + "mul", + ...pairs.map(([expr, exp]) => + exp > 1 ? op.pow(expr, int(exp)) : expr, + ), + ); + } + } +} export const powPlugins = [powToMul(), mulToPow]; @@ -254,12 +233,12 @@ export function bitShiftToMulOrDiv( visit(node) { if (isOp("bit_shift_left", "bit_shift_right")(node)) { const [a, b] = node.args; - if (!literalOnly || isIntLiteral()(b)) { + if (!literalOnly || isInt()(b)) { if (node.op === "bit_shift_left" && toMul) { - return op("mul", a, op("pow", int(2), b)); + return op.mul(a, op.pow(int(2), b)); } if (node.op === "bit_shift_right" && toDiv) { - return op("div", a, op("pow", int(2), b)); + return op.div(a, op.pow(int(2), b)); } } } @@ -284,34 +263,32 @@ export function mulOrDivToBitShift(fromMul = true, fromDiv = true): Plugin { visit(node) { if (isOp("div")(node) && fromDiv) { const [a, b] = node.args; - if (isIntLiteral()(b)) { + if (isInt()(b)) { const [n, exp] = getOddAnd2Exp(b.value); if (exp > 1 && n === 1n) { - return op("bit_shift_right", a, int(exp)); + return op.bit_shift_right(a, int(exp)); } } - if (isOp("pow")(b) && isIntLiteral(2n)(b.args[0])) { - return op("bit_shift_right", a, b.args[1]); + if (isOp("pow")(b) && isInt(2n)(b.args[0])) { + return op.bit_shift_right(a, b.args[1]); } } if (isOp("mul")(node) && fromMul) { - if (isIntLiteral()(node.args[0])) { + if (isInt()(node.args[0])) { const [n, exp] = getOddAnd2Exp(node.args[0].value); if (exp > 1) { - return op( - "bit_shift_left", - op("mul", int(n), ...node.args.slice(1)), + return op.bit_shift_left( + op.unsafe("mul", int(n), ...node.args.slice(1)), int(exp), ); } } const powNode = node.args.find( - (x) => isOp("pow")(x) && isIntLiteral(2n)(x.args[0]), - ) as Op | undefined; + (x) => isOp("pow")(x) && isInt(2n)(x.args[0]), + ) as Op<"pow"> | undefined; if (powNode !== undefined) { - return op( - "bit_shift_left", - op("mul", ...node.args.filter((x) => x !== powNode)), + return op.bit_shift_left( + op.unsafe("mul", ...node.args.filter((x) => x !== powNode)), powNode.args[1], ); } @@ -449,7 +426,7 @@ export function decomposeIntLiteral( ])})`, visit(node) { let decompositions: IntDecomposition[] = []; - if (isIntLiteral()(node) && (node.value <= -1000 || node.value >= 1000)) { + if (isInt()(node) && (node.value <= -1000 || node.value >= 1000)) { decompositions = decomposeInt( node.value, hasScientific, @@ -467,19 +444,26 @@ export function decomposeIntLiteral( } return decompositions.map(([k, b, e, d]) => - op("add", op("mul", int(k), op("pow", int(b), int(e))), int(d)), + op.add(op.mul(int(k), op.pow(int(b), int(e))), int(d)), ); }, }; } -export const pickAnyInt: Plugin = { - name: "pickAnyInt", - visit(node) { - if (node.kind === "AnyInteger") { - return node.low.toString().length < node.high.toString().length - ? int(node.low) - : int(node.high); - } - }, -}; +export function pickAnyInt(node: Node) { + if (node.kind === "AnyInteger") { + return node.low.toString().length < node.high.toString().length + ? int(node.low) + : int(node.high); + } +} + +export function useImplicitBoolToInt(node: Node, spine: Spine) { + if ( + isOp("bool_to_int")(node) && + !spine.isRoot && + isOp("at[Array]", "at[List]")(spine.parent!.node) // This can be extend to other ops, like "mul". + ) { + return implicitConversion(node.op, node.args[0]); + } +} diff --git a/src/plugins/block.test.md b/src/plugins/block.test.md index 14c40fde..bcda575d 100644 --- a/src/plugins/block.test.md +++ b/src/plugins/block.test.md @@ -33,22 +33,22 @@ many_to_many_assignment { $a; $b; } { $b; $a; }; ## Variable inlining ```polygolf -$x:Ascii <- (argv_get 0); +$x <- (at[argv] 0):Ascii; print_int (- (text_to_int $x)); ``` ```polygolf block.inlineVariables -print_int (- (text_to_int (argv_get 0):Ascii)); +print_int (- (text_to_int (at[argv] 0):Ascii)); ``` ```polygolf -$x <- read_line; +$x <- read[line]; println $x; println $x; ``` ```polygolf block.inlineVariables -$x <- read_line; +$x <- read[line]; println $x; println $x; ``` diff --git a/src/plugins/block.ts b/src/plugins/block.ts index 9c2583d8..6f0e645a 100644 --- a/src/plugins/block.ts +++ b/src/plugins/block.ts @@ -7,7 +7,7 @@ import { type Node, type Identifier, isAssignment, - isAssignmentToIdentifier, + isAssignmentToIdent, isIdent, isUserIdent, manyToManyAssignment, @@ -21,6 +21,7 @@ import { type Plugin } from "../common/Language"; import { type Spine } from "../common/Spine"; import { stringify } from "../common/stringify"; import { getWrites, hasSideEffect } from "../common/symbols"; +import type { CompilationContext } from "../common/compile"; /** * Collects neighbouring block children matching a predicate and replaces them with a different set of children. @@ -82,18 +83,15 @@ export function blockChildrenCollectAndReplace( } const declared: Set = new Set(); -export const addVarDeclarations: Plugin = { - name: "addVarDeclarations", - visit(node, spine) { - if (spine.isRoot) declared.clear(); - if (node.kind === "Assignment") { - if (isIdent()(node.variable) && !declared.has(node.variable.name)) { - declared.add(node.variable.name); - return varDeclarationWithAssignment(node); - } +export function addVarDeclarations(node: Node, spine: Spine) { + if (spine.isRoot) declared.clear(); + if (node.kind === "Assignment") { + if (isIdent()(node.variable) && !declared.has(node.variable.name)) { + declared.add(node.variable.name); + return varDeclarationWithAssignment(node); } - }, -}; + } +} /** * Replaces `v1 = c; v2 = c; ... ; vn = c` with `v1,v2,...vn=c` @@ -104,7 +102,7 @@ export function addOneToManyAssignments( return blockChildrenCollectAndReplace>( "addOneToManyAssignments", (expr, spine, previous) => - isAssignmentToIdentifier(expr) && + isAssignmentToIdent()(expr) && previous.every((x) => x.variable.name !== expr.variable.name) && (previous.length < 1 || stringify(expr.expr) === stringify(previous[0].expr)), @@ -130,7 +128,7 @@ export function addVarDeclarationOneToManyAssignments( "addVarDeclarationOneToManyAssignments", (expr, spine, previous) => expr.kind === "VarDeclarationWithAssignment" && - isAssignmentToIdentifier(expr.assignment) && + isAssignmentToIdent()(expr.assignment) && (previous.length < 1 || stringify(expr.assignment.expr) === stringify(previous[0].assignment.expr)), @@ -155,7 +153,7 @@ export function addManyToManyAssignments( return blockChildrenCollectAndReplace>( "addManyToManyAssignments", (expr, spine, previous) => - isAssignmentToIdentifier(expr) && + isAssignmentToIdent()(expr) && !previous.some((x) => spine.someNode(isUserIdent(x.variable.name))), (exprs) => [ manyToManyAssignment( @@ -179,7 +177,7 @@ export function addVarDeclarationManyToManyAssignments( "addVarDeclarationManyToManyAssignments", (expr, spine, previous) => expr.kind === "VarDeclarationWithAssignment" && - isAssignmentToIdentifier(expr.assignment) && + isAssignmentToIdent()(expr.assignment) && !previous.some((x) => spine.someNode(isUserIdent(x.assignment.variable.name)), ), @@ -213,96 +211,88 @@ export function groupVarDeclarations( ); } -export const noStandaloneVarDeclarations: Plugin = { - name: "noStandaloneVarDeclarations", - visit(node, spine) { - if ( - (node.kind === "VarDeclaration" || - node.kind === "VarDeclarationWithAssignment") && - spine.parent?.node.kind !== "VarDeclarationBlock" - ) { - return varDeclarationBlock([node]); - } - }, -}; +export function noStandaloneVarDeclarations(node: Node, spine: Spine) { + if ( + (node.kind === "VarDeclaration" || + node.kind === "VarDeclarationWithAssignment") && + spine.parent?.node.kind !== "VarDeclarationBlock" + ) { + return varDeclarationBlock([node]); + } +} -export const tempVarToMultipleAssignment: Plugin = { - name: "tempVarToMultipleAssignment", - visit(node) { - if (node.kind === "Block") { - const newNodes: Node[] = []; - let changed = false; - for (let i = 0; i < node.children.length; i++) { - const a = node.children[i]; - if (i >= node.children.length - 2) { - newNodes.push(a); - continue; - } - const b = node.children[i + 1]; - const c = node.children[i + 2]; - if ( - isAssignmentToIdentifier(a) && - isAssignment(b) && - isAssignmentToIdentifier(c) && - isIdent(c.variable)(b.expr) && - isIdent(a.variable)(c.expr) - ) { - newNodes.push( - manyToManyAssignment([b.variable, c.variable], [b.expr, a.expr]), - ); - changed = true; - i += 2; - } else { - newNodes.push(a); - } +export function tempVarToMultipleAssignment(node: Node) { + if (node.kind === "Block") { + const newNodes: Node[] = []; + let changed = false; + for (let i = 0; i < node.children.length; i++) { + const a = node.children[i]; + if (i >= node.children.length - 2) { + newNodes.push(a); + continue; + } + const b = node.children[i + 1]; + const c = node.children[i + 2]; + if ( + isAssignmentToIdent()(a) && + isAssignment(b) && + isAssignmentToIdent()(c) && + isIdent(c.variable)(b.expr) && + isIdent(a.variable)(c.expr) + ) { + newNodes.push( + manyToManyAssignment([b.variable, c.variable], [b.expr, a.expr]), + ); + changed = true; + i += 2; + } else { + newNodes.push(a); } - if (changed) return block(newNodes); } - }, -}; + if (changed) return block(newNodes); + } +} -export const inlineVariables: Plugin = { - name: "inlineVariables", - visit(node, spine) { - if (spine.isRoot) { - const writes = groupby(getWrites(spine), (x) => x.node.name); - const suggestions = []; - for (const a of writes.values()) { - if (a.length === 1) { - const variable = a[0].node; - const write = a[0].parent!; - if ( - write.node.kind === "Assignment" && - write.parent?.node.kind === "Block" && - spine.someNode( - (n) => n !== variable && isUserIdent(variable)(n), // in tests variables are often never read from and we don't want to make those disappear - ) && - !write.getChild("expr").someNode(isUserIdent(variable)) && - !hasSideEffect(write.getChild("expr")) - ) { - const assignmentToInlineSpine = write as Spine< - Assignment - >; - const assignment = assignmentToInlineSpine.node; - const assignmentParent = assignmentToInlineSpine.parent?.node; - suggestions.push( - spine.withReplacer((x) => - x === assignmentParent && x.kind === "Block" - ? blockOrSingle(x.children.filter((y) => y !== assignment)) - : x.kind === "Identifier" && - !x.builtin && - x.name === assignment.variable.name - ? { - ...assignment.expr, - type: assignment.expr.type ?? assignment.variable.type, - } - : undefined, - ).node, - ); - } - } +export function inlineVariables( + node: Node, + spine: Spine, + context: CompilationContext, +) { + context.skipChildren(); + const writes = groupby(getWrites(spine), (x) => x.node.name); + const suggestions = []; + for (const a of writes.values()) { + if (a.length === 1) { + const variable = a[0].node; + const write = a[0].parent!; + if ( + write.node.kind === "Assignment" && + write.parent?.node.kind === "Block" && + spine.someNode( + (n) => n !== variable && isUserIdent(variable)(n), // in tests variables are often never read from and we don't want to make those disappear + ) && + !write.getChild("expr").someNode(isUserIdent(variable)) && + !hasSideEffect(write.getChild("expr")) + ) { + const assignmentToInlineSpine = write as Spine>; + const assignment = assignmentToInlineSpine.node; + const assignmentParent = assignmentToInlineSpine.parent?.node; + suggestions.push( + spine.withReplacer((x) => + x === assignmentParent && x.kind === "Block" + ? blockOrSingle(x.children.filter((y) => y !== assignment)) + : x.kind === "Identifier" && + !x.builtin && + x.name === assignment.variable.name + ? { + ...assignment.expr, + type: assignment.expr.type ?? assignment.variable.type, + } + : undefined, + ).node, + ); } - return suggestions; } - }, -}; + } + return suggestions; +} diff --git a/src/plugins/conditions.test.md b/src/plugins/conditions.test.md new file mode 100644 index 00000000..85c22e2c --- /dev/null +++ b/src/plugins/conditions.test.md @@ -0,0 +1,17 @@ +# Conditions + +```polygolf +conditional true 3 4; +``` + +```polygolf conditions.safeConditionalOpToAt("Array") +array_get (array 4 3) (bool_to_int true); +``` + +```polygolf +conditional true 3 4; +``` + +```polygolf conditions.conditionalOpToAndOr(()=>true) +unsafe_or (unsafe_and true 3) 4; +``` diff --git a/src/plugins/conditions.ts b/src/plugins/conditions.ts new file mode 100644 index 00000000..718412a3 --- /dev/null +++ b/src/plugins/conditions.ts @@ -0,0 +1,107 @@ +import type { Visitor } from "@/common/Spine"; +import type { Plugin } from "../common/Language"; +import { + array, + conditional, + ifStatement, + int, + keyValue, + list, + op, + table, +} from "../IR"; + +export function safeConditionalOpToAt( + type: "Array" | "List" | "Table", +): Plugin { + return { + name: "safeConditionalOpToAt", + visit(node) { + if (node.kind === "ConditionalOp" && node.isSafe) { + switch (type) { + case "Array": + return op["at[Array]"]( + array([node.alternate, node.consequent]), + op.bool_to_int(node.condition), + ); + case "List": + return op["at[List]"]( + list([node.alternate, node.consequent]), + op.bool_to_int(node.condition), + ); + case "Table": + return op["at[Table]"]( + table([ + keyValue(op.true, node.consequent), + keyValue(op.false, node.alternate), + ]), + node.condition, + ); + } + } + }, + }; +} + +export function conditionalOpToAndOr( + isProvablyThruthy: Visitor, + falseyFallback?: "List" | "Array", +): Plugin { + return { + name: "conditionalOpToAndOr", + bakeType: true, + visit(node, spine, context) { + if (node.kind === "ConditionalOp") { + if ( + isProvablyThruthy( + node.consequent, + spine.getChild("consequent"), + context, + ) + ) + return op.unsafe_or( + op.unsafe_and(node.condition, node.consequent), + node.alternate, + ); + if (falseyFallback !== undefined) { + const opCode = `at[${falseyFallback}]` as const; + const collection = falseyFallback === "List" ? list : array; + return op[opCode]( + op.unsafe_or( + op.unsafe_and(node.condition, collection([node.consequent])), + collection([node.alternate]), + ), + int(0n), + ); + } + } + }, + }; +} + +export const flipConditionalOp: Plugin = { + name: "flipConditionalOp", + visit(node) { + if (node.kind === "ConditionalOp" && node.isSafe) { + return conditional( + op.not(node.condition), + node.alternate, + node.consequent, + true, + ); + } + }, +}; + +export const flipIfStatement: Plugin = { + name: "flipIfStatement", + visit(node) { + if (node.kind === "If" && node.alternate !== undefined) { + return ifStatement( + op.not(node.condition), + node.alternate, + node.consequent, + ); + } + }, +}; diff --git a/src/plugins/idents.test.md b/src/plugins/idents.test.md index cb778e48..25384aad 100644 --- a/src/plugins/idents.test.md +++ b/src/plugins/idents.test.md @@ -23,11 +23,14 @@ println_int 12345; println_int 12345; println_int 12345; println "text"; +println (at[argv] 0); + ``` ```py +import sys p=print t="text" a=12345 @@ -37,4 +40,5 @@ p(a) p(a) p(a) p(t) +p(sys.argv[1]) ``` diff --git a/src/plugins/idents.ts b/src/plugins/idents.ts index 5edab490..cd549135 100644 --- a/src/plugins/idents.ts +++ b/src/plugins/idents.ts @@ -8,8 +8,11 @@ import { type Identifier, type IR, isUserIdent, + type Node, type NodeFuncRecord, getNodeFunc, + isText, + builtin, } from "../IR"; function getIdentMap( @@ -18,7 +21,7 @@ function getIdentMap( ): Map { // First, try mapping as many idents as possible to their preferred versions const inputNames = [...getDeclaredIdentifiers(spine.node)]; - const outputNames = new Set(); + const outputNames = new Set(identGen.reserved); const result = new Map(); for (const iv of inputNames) { for (const preferred of identGen.preferred(iv)) { @@ -60,12 +63,12 @@ function getIdentMap( } export function renameIdents( - identGen: IdentifierGenerator = defaultIdentGen, + identGen: IdentifierGenerator = defaultIdentGen(), ): Plugin { return { name: "renameIdents(...)", - visit(program, spine) { - if (!spine.isRoot) return; + visit(program, spine, context) { + context.skipChildren(); const identMap = getIdentMap(spine.root, identGen); return spine.withReplacer((node) => { if (isUserIdent()(node)) { @@ -86,20 +89,23 @@ export function renameIdents( const letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"; -const defaultIdentGen: IdentifierGenerator = { - preferred(original: string) { - const firstLetter = [...original].find((x) => /[A-Za-z]/.test(x)); - if (firstLetter === undefined) return []; - const lower = firstLetter.toLowerCase(); - const upper = firstLetter.toUpperCase(); - return [firstLetter, firstLetter === lower ? upper : lower]; - }, - short: letters.split(""), - general: (i) => `v${i}`, -}; +export function defaultIdentGen(...reserved: string[]): IdentifierGenerator { + return { + preferred(original: string) { + const firstLetter = [...original].find((x) => /[A-Za-z]/.test(x)); + if (firstLetter === undefined) return []; + const lower = firstLetter.toLowerCase(); + const upper = firstLetter.toUpperCase(); + return [firstLetter, firstLetter === lower ? upper : lower]; + }, + short: letters.split(""), + general: (i) => `v${i}`, + reserved, + }; +} export const lettersOnlyIdentGen: IdentifierGenerator = { - ...defaultIdentGen, + ...defaultIdentGen(), general: (i) => { let s = ""; // 0 is the first general id. Skip over the shorts, so map it to 53. @@ -136,8 +142,8 @@ export function alias( (key.length - save[0]) * (freq - 1) - save[0] - save[1]; return { name: "alias(...)", - visit(prog, spine) { - if (!spine.isRoot) return; + visit(prog, spine, context) { + context.skipChildren(); // get frequency of expr const timesUsed = new Map(); for (const key of spine.compactMap(getKey)) { @@ -158,3 +164,11 @@ export function alias( }, }; } + +export function useBuiltinAliases(builtins: Record) { + return function (node: Node) { + if (isText()(node) && node.value in builtins) { + return builtin(builtins[node.value]); + } + }; +} diff --git a/src/plugins/imports.ts b/src/plugins/imports.ts index 8b6e07c8..eb19f2bc 100644 --- a/src/plugins/imports.ts +++ b/src/plugins/imports.ts @@ -24,8 +24,8 @@ export function addImports( // TODO caching return { name: "addImports(...)", - visit(node, spine) { - if (!spine.isRoot) return; + visit(node, spine, context) { + context.skipChildren(); const modules = spine.compactMap(rulesFunc); const outputNode = outputFunc([...new Set(modules)]); if (outputNode !== undefined) { diff --git a/src/plugins/loops.test.md b/src/plugins/loops.test.md index 26d1a399..07ce9f4a 100644 --- a/src/plugins/loops.test.md +++ b/src/plugins/loops.test.md @@ -4,30 +4,54 @@ ```polygolf for $i 0 10 { - print_int $x; + print_int $i; }; ``` ```polygolf loops.forRangeToForRangeInclusive() for_range_inclusive $i 0 9 1 ( - print_int $x + print_int $i ); ``` ```polygolf loops.forRangeToWhile $i <- 0; while ($i < 10) { - print_int $x; + print_int $i; $i <- (1 + $i); }; ``` ```polygolf loops.forRangeToForCLike for_c_like ($i <- 0) ($i < 10) ($i <- (1 + $i)) ( - print_int $x + print_int $i ); ``` +## For range to while, inside a block + +```polygolf +for $i 0 10 { + print_int $x; +}; +for $j 1 11 { + print_int $y; +}; +``` + +```polygolf loops.forRangeToWhile +$i <- 0; +while ($i < 10) { + print_int $x; + $i <- (1 + $i); +}; +$j <- 1; +while ($j < 11) { + print_int $y; + $j <- (1 + $j); +}; +``` + ## For range to while, not at root ```polygolf @@ -49,12 +73,12 @@ while ($i < 10) { ## For each ```polygolf -for $i 0 (# $collection) { - print (list_get $collection $i); +for $i 0 (size[List] $collection) { + print[Text] (list_get $collection $i); }; ``` -```polygolf loops.forRangeToForEach("list_get") +```polygolf loops.forRangeToForEach("at[List]") for_each (id "i+each") $collection ( print (id "i+each") ); @@ -63,11 +87,11 @@ for_each (id "i+each") $collection ( ```polygolf $collection <- (array "a" "b" "c"); for $i 0 3 { - print (array_get $collection $i); + print[Text] (array_get $collection $i); }; ``` -```polygolf loops.forRangeToForEach("array_get") +```polygolf loops.forRangeToForEach("at[Array]") $collection <- (array "a" "b" "c"); for_each (id "i+each") $collection ( print (id "i+each") @@ -77,11 +101,11 @@ for_each (id "i+each") $collection ( ```polygolf $collection <- (array "a" "b" "c" "d"); for $i 0 3 { - print (array_get $collection $i); + print[Text] (array_get $collection $i); }; ``` -```polygolf loops.forRangeToForEach("array_get") +```polygolf loops.forRangeToForEach("at[Array]") $collection <- (array "a" "b" "c" "d"); for $i 0 3 ( print (array_get $collection $i) @@ -94,7 +118,7 @@ for $i 0 10 ( ); ``` -```polygolf loops.forRangeToForEach("text_get_byte") +```polygolf loops.forRangeToForEach("at[byte]") for_each (id "i+each") "9876543210" ( print (id "i+each") ); @@ -106,7 +130,7 @@ for $i 0 5 ( ); ``` -```polygolf loops.forRangeToForEach("text_get_byte") +```polygolf loops.forRangeToForEach("at[byte]") for $i 0 5 ( print (text_get_byte "9876543210" $i) ); @@ -118,7 +142,7 @@ for $i 0 5 { }; ``` -```polygolf loops.forRangeToForEach("list_get") +```polygolf loops.forRangeToForEach("at[List]") for_each (id "i+each") (list 5 4 3 2 1) ( print_int (id "i+each") ); @@ -127,13 +151,13 @@ for_each (id "i+each") (list 5 4 3 2 1) ( ## For each pair ```polygolf -for $i 0 (# $collection) { +for $i 0 (size[List] $collection) { print_int $i; print_int (list_get $collection $i); }; ``` -```polygolf loops.forRangeToForEach("list_get") +```polygolf loops.forRangeToForEach("at[List]") for $i 0 (# $collection) { print_int $i; print_int (list_get $collection $i); @@ -163,14 +187,14 @@ for_each $x argv ( ```polygolf loops.forArgvToForRange() for (id "x+index") 0 100 { - $x <- (argv_get (id "x+index")); + $x <- (at[argv] (id "x+index")); println $x; }; ``` ```polygolf loops.forArgvToForRange(false) for (id "x+index") 0 argc { - $x <- (argv_get (id "x+index")); + $x <- (at[argv] (id "x+index")); println $x; }; ``` diff --git a/src/plugins/loops.ts b/src/plugins/loops.ts index e7478140..0db89f86 100644 --- a/src/plugins/loops.ts +++ b/src/plugins/loops.ts @@ -15,7 +15,7 @@ import { op, type Node, type Identifier, - isIntLiteral, + isInt, type OpCode, type Text, type List, @@ -24,8 +24,8 @@ import { isOp, isSubtype, integerType, - add1, - sub1, + succ, + prec, isText, isIdent, isUserIdent, @@ -33,7 +33,7 @@ import { ifStatement, functionCall, } from "../IR"; -import { byteLength, charLength } from "../common/objective"; +import { byteLength, charLength } from "../common/strings"; import { PolygolfError } from "../common/errors"; import { tempId } from "../common/symbols"; @@ -44,12 +44,12 @@ export function forRangeToForRangeInclusive(skip1Step = false): Plugin { if ( node.kind === "ForRange" && !node.inclusive && - (!skip1Step || !isIntLiteral(1n)(node.increment)) + (!skip1Step || !isInt(1n)(node.increment)) ) return forRange( node.variable, node.start, - sub1(node.end), + prec(node.end), node.increment, node.body, true, @@ -58,14 +58,7 @@ export function forRangeToForRangeInclusive(skip1Step = false): Plugin { }; } -export const forRangeToWhile: Plugin = { - name: "forRangeToWhile", - visit(_node, spine) { - return spine.flatMapWithChildrenReplacer(forRangeToWhileVisitor); - }, -}; - -function forRangeToWhileVisitor(node: IR.Node, spine: Spine) { +export function forRangeToWhile(node: Node, spine: Spine) { if (node.kind === "ForRange" && node.variable !== undefined) { const low = getType(node.start, spine); const high = getType(node.end, spine); @@ -74,36 +67,34 @@ function forRangeToWhileVisitor(node: IR.Node, spine: Spine) { } const increment = assignment( node.variable, - op("add", node.variable, node.increment), + op.add(node.variable, node.increment), ); - return [ + return block([ assignment(node.variable, node.start), whileLoop( - op(node.inclusive ? "leq" : "lt", node.variable, node.end), + op[node.inclusive ? "leq" : "lt"](node.variable, node.end), block([node.body, increment]), ), - ]; + ]); } } -export const forRangeToForCLike: Plugin = { - name: "forRangeToForCLike", - visit(node, spine) { - if (node.kind === "ForRange" && node.variable !== undefined) { - const low = getType(node.start, spine); - const high = getType(node.end, spine); - if (low.kind !== "integer" || high.kind !== "integer") { - throw new Error(`Unexpected type (${low.kind},${high.kind})`); - } - return forCLike( - assignment(node.variable, node.start), - op(node.inclusive ? "leq" : "lt", node.variable, node.end), - assignment(node.variable, op("add", node.variable, node.increment)), - node.body, - ); +export function forRangeToForCLike(node: Node, spine: Spine) { + if (node.kind === "ForRange") { + const low = getType(node.start, spine); + const high = getType(node.end, spine); + if (low.kind !== "integer" || high.kind !== "integer") { + throw new Error(`Unexpected type (${low.kind},${high.kind})`); } - }, -}; + const variable = node.variable ?? id(); + return forCLike( + assignment(variable, node.start), + op[node.inclusive ? "leq" : "lt"](variable, node.end), + assignment(variable, op.add(variable, node.increment)), + node.body, + ); + } +} /** * Python: @@ -115,32 +106,29 @@ export const forRangeToForCLike: Plugin = { * commands(i, x) */ // TODO: Handle inclusive like Lua's `for i=1,#L do commands(i, L[i]) end -export const forRangeToForEachPair: Plugin = { - name: "forRangeToForEachPair", - visit(node, spine) { - if ( - node.kind === "ForRange" && - node.variable !== undefined && - !node.inclusive && - isIntLiteral(0n)(node.start) && - isOp("list_length")(node.end) && - isIdent()(node.end.args[0]) - ) { - const variable = node.variable; - const collection = node.end.args[0]; - const elementIdentifier = id(variable.name + "+each"); - const newBody = spine.getChild("body").withReplacer((innerNode) => { - if (isListGet(innerNode, collection.name, variable.name)) - return elementIdentifier; - }).node; - return forEachPair(variable, elementIdentifier, collection, newBody); - } - }, -}; +export function forRangeToForEachPair(node: Node, spine: Spine) { + if ( + node.kind === "ForRange" && + node.variable !== undefined && + !node.inclusive && + isInt(0n)(node.start) && + isOp("size[List]")(node.end) && + isIdent()(node.end.args[0]) + ) { + const variable = node.variable; + const collection = node.end.args[0]; + const elementIdentifier = id(variable.name + "+each"); + const newBody = spine.getChild("body").withReplacer((innerNode) => { + if (isListGet(innerNode, collection.name, variable.name)) + return elementIdentifier; + }).node; + return forEachPair(variable, elementIdentifier, collection, newBody); + } +} function isListGet(node: IR.Node, collection: string, index: string) { return ( - isOp("list_get")(node) && + isOp("at[List]")(node) && isIdent(collection)(node.args[0]) && isIdent(index)(node.args[1]) ); @@ -155,18 +143,17 @@ function isListGet(node: IR.Node, collection: string, index: string) { * for x in collection: * commands(x) */ -type GetOp = OpCode & - ("array_get" | "list_get" | "text_get_byte" | "text_get_codepoint"); +type GetOp = OpCode & ("at[Array]" | "at[List]" | "at[byte]" | "at[codepoint]"); export function forRangeToForEach(...ops: GetOp[]): Plugin { - if (ops.includes("text_get_byte") && ops.includes("text_get_codepoint")) + if (ops.includes("at[byte]") && ops.includes("at[codepoint]")) throw new Error( - "Programming error. Choose only one of 'text_get_byte' && 'text_get_codepoint'.", + "Programming error. Choose only one of 'at[byte]' && 'at[codepoint]'.", ); const lengthOpToGetOp = new Map([ - ["array_length", "array_get"], - ["list_length", "list_get"], - ["text_byte_length", "text_get_byte"], - ["array_length", "text_get_codepoint"], + ["size[Array]", "at[Array]"], + ["size[List]", "at[List]"], + ["size[byte]", "at[byte]"], + ["size[codepoint]", "at[codepoint]"], ]); return { name: "forRangeToForEach", @@ -175,21 +162,21 @@ export function forRangeToForEach(...ops: GetOp[]): Plugin { node.kind === "ForRange" && node.variable !== undefined && !node.inclusive && - isIntLiteral(0n)(node.start) && + isInt(0n)(node.start) && ((isOp()(node.end) && ops.includes(lengthOpToGetOp.get(node.end.op) as any) && - isIdent()(node.end.args[0])) || - isIntLiteral()(node.end)) + isIdent()(node.end.args[0]!)) || + isInt()(node.end)) ) { const indexVar = node.variable; const bodySpine = spine.getChild("body"); - const knownLength = isIntLiteral()(node.end) + const knownLength = isInt()(node.end) ? Number(node.end.value) : undefined; - const allowedOps = isIntLiteral()(node.end) + const allowedOps = isInt()(node.end) ? ops : [lengthOpToGetOp.get(node.end.op) as GetOp]; - const collectionVar = isIntLiteral()(node.end) + const collectionVar = isInt()(node.end) ? undefined : (node.end.args[0] as Identifier); const indexedCollection = getIndexedCollection( @@ -205,7 +192,7 @@ export function forRangeToForEach(...ops: GetOp[]): Plugin { if ( isOp()(n) && n.args[0] === indexedCollection && - isUserIdent(indexVar.name)(n.args[1]) + isUserIdent(indexVar.name)(n.args[1]!) ) return elementIdentifier; }).node; @@ -239,8 +226,7 @@ function getIndexedCollection( const collection = parent.args[0]; if ( (isText()(collection) || collection.kind === "List") && - literalLength(collection, allowedOps.includes("text_get_byte")) === - knownLength + literalLength(collection, allowedOps.includes("at[byte]")) === knownLength ) return collection; if ( @@ -250,8 +236,10 @@ function getIndexedCollection( return collection; const collectionType = getType(collection, s.root.node); if ( + knownLength !== undefined && collectionType.kind === "Array" && - collectionType.length === knownLength + collectionType.length.kind === "integer" && + collectionType.length.high + 1n === BigInt(knownLength) ) return collection; return null; @@ -267,14 +255,11 @@ function literalLength(expr: Text | List, countTextBytes: boolean): number { return (countTextBytes ? byteLength : charLength)(expr.value); } -export const forArgvToForEach: Plugin = { - name: "forArgvToForEach", - visit(node) { - if (node.kind === "ForArgv") { - return forEach(node.variable, op("argv"), node.body); - } - }, -}; +export function forArgvToForEach(node: Node) { + if (node.kind === "ForArgv") { + return forEach(node.variable, op.argv, node.body); + } +} export function forArgvToForRange(overshoot = true, inclusive = false): Plugin { return { @@ -283,7 +268,7 @@ export function forArgvToForRange(overshoot = true, inclusive = false): Plugin { if (node.kind === "ForArgv") { const indexVar = id(node.variable.name + "+index"); const newBody = block([ - assignment(node.variable, op("argv_get", indexVar)), + assignment(node.variable, op["at[argv]"](indexVar)), node.body, ]); return forRange( @@ -291,9 +276,9 @@ export function forArgvToForRange(overshoot = true, inclusive = false): Plugin { int(0), overshoot ? inclusive - ? sub1(int(node.argcUpperBound)) + ? prec(int(node.argcUpperBound)) : int(node.argcUpperBound) - : op("argc"), + : op.argc, int(1), newBody, inclusive, @@ -303,71 +288,65 @@ export function forArgvToForRange(overshoot = true, inclusive = false): Plugin { }; } -export const assertForArgvTopLevel: Plugin = { - name: "assertForArgvTopLevel", - visit(node, spine) { - if (spine.isRoot) { - let forArgvSeen = false; - for (const kind of spine.compactMap((x) => x.kind)) { - if (kind === "ForArgv") { - if (forArgvSeen) - throw new PolygolfError( - "Only a single for_argv node allowed.", - node.source, - ); - forArgvSeen = true; - } +export function assertForArgvTopLevel(node: Node, spine: Spine) { + if (spine.isRoot) { + let forArgvSeen = false; + for (const kind of spine.compactMap((x) => x.kind)) { + if (kind === "ForArgv") { + if (forArgvSeen) + throw new PolygolfError( + "Only a single for_argv node allowed.", + node.source, + ); + forArgvSeen = true; } } - if (node.kind === "ForArgv") { - if ( - !( - spine.isRoot || - (spine.parent?.node.kind === "Block" && spine.parent.isRoot) - ) - ) { - throw new PolygolfError( - "Node for_argv only allowed at the top level.", - node.source, - ); - } - } - return undefined; - }, -}; - -export const shiftRangeOneUp: Plugin = { - name: "shiftRangeOneUp", - visit(node, spine) { + } + if (node.kind === "ForArgv") { if ( - node.kind === "ForRange" && - node.variable !== undefined && - isIntLiteral(1n)(node.increment) && - spine.someNode( - (x) => - isOp("add")(x) && - isIntLiteral(1n)(x.args[0]) && - isIdent(node.variable!)(x.args[1]), + !( + spine.isRoot || + (spine.parent?.node.kind === "Block" && spine.parent.isRoot) ) ) { - const bodySpine = spine.getChild("body"); - const newVar = id(node.variable.name + "+shift"); - const newBodySpine = bodySpine.withReplacer((x) => - newVar !== undefined && isIdent(node.variable!)(x) - ? sub1(newVar) - : undefined, - ); - return forRange( - newVar, - add1(node.start), - add1(node.end), - int(1n), - newBodySpine.node, - node.inclusive, + throw new PolygolfError( + "Node for_argv only allowed at the top level.", + node.source, ); } - }, -}; + } + return undefined; +} + +export function shiftRangeOneUp(node: Node, spine: Spine) { + if ( + node.kind === "ForRange" && + node.variable !== undefined && + isInt(1n)(node.increment) && + spine.someNode( + (x) => + isOp("add")(x) && + isInt(1n)(x.args[0]) && + isIdent(node.variable!)(x.args[1]), + ) + ) { + const bodySpine = spine.getChild("body"); + const newVar = id(node.variable.name + "+shift"); + const newBodySpine = bodySpine.withReplacer((x) => + newVar !== undefined && isIdent(node.variable!)(x) + ? prec(newVar) + : undefined, + ); + return forRange( + newVar, + succ(node.start), + succ(node.end), + int(1n), + newBodySpine.node, + node.inclusive, + ); + } +} export function forRangeToForDifferenceRange( transformPredicate: ( @@ -386,7 +365,7 @@ export function forRangeToForDifferenceRange( return forDifferenceRange( node.variable, node.start, - op("sub", node.end, node.start), + op.sub(node.end, node.start), node.increment, node.body, node.inclusive, @@ -396,60 +375,52 @@ export function forRangeToForDifferenceRange( }; } -export const forRangeToForRangeOneStep: Plugin = { - name: "forRangeToForRangeOneStep", - visit(node, spine) { - if ( - node.kind === "ForRange" && - node.variable !== undefined && - isSubtype(getType(node.increment, spine.root.node), integerType(2n)) - ) { - const newVar = id(node.variable.name + "+1step"); +export function forRangeToForRangeOneStep(node: Node, spine: Spine) { + if ( + node.kind === "ForRange" && + node.variable !== undefined && + isSubtype(getType(node.increment, spine.root.node), integerType(2n)) + ) { + const newVar = id(node.variable.name + "+1step"); + return forRange( + newVar, + int(0n), + node.inclusive + ? op.div(op.sub(node.end, node.start), node.increment) + : succ(op.div(op.sub(prec(node.end), node.start), node.increment)), + int(1n), + block([ + assignment( + node.variable, + op.add(op.mul(newVar, node.increment), node.start), + ), + node.body, + ]), + node.inclusive, + ); + } +} + +export function removeUnusedForVar(node: Node, spine: Spine) { + if (node.kind === "ForRange" && node.variable !== undefined) { + const variable = node.variable; + if (!spine.getChild("body").someNode(isUserIdent(variable))) { return forRange( - newVar, - int(0n), - node.inclusive - ? op("div", op("sub", node.end, node.start), node.increment) - : add1( - op("div", op("sub", sub1(node.end), node.start), node.increment), - ), - int(1n), - block([ - assignment( - node.variable, - op("add", op("mul", newVar, node.increment), node.start), - ), - node.body, - ]), + undefined, + node.start, + node.end, + node.increment, + node.body, node.inclusive, ); } - }, -}; - -export const removeUnusedForVar: Plugin = { - name: "removeUnusedForVar", - visit(node, spine) { - if (node.kind === "ForRange" && node.variable !== undefined) { - const variable = node.variable; - if (!spine.getChild("body").someNode(isUserIdent(variable))) { - return forRange( - undefined, - node.start, - node.end, - node.increment, - node.body, - node.inclusive, - ); - } - } - }, -}; + } +} export const whileToRecursion: Plugin = { name: "whileToRecursion", - visit(_node, spine) { - return spine.flatMapWithChildrenReplacer(whileToRecursionVisitor); + visit(_node) { + return whileToRecursionVisitor(_node); }, }; diff --git a/src/plugins/ops.test.md b/src/plugins/ops.test.md index d631dcf2..a82ce67e 100644 --- a/src/plugins/ops.test.md +++ b/src/plugins/ops.test.md @@ -12,7 +12,7 @@ $x <- ($x .. " world"); $x <- ("prepend" .. $x); ``` -```polygolf ops.addMutatingInfix({add:"+","concat":"+"}) +```polygolf ops.addMutatingInfix({add:"+","concat[Text]":"+"}) $n:-oo..oo <- 0; $a:-oo..oo <- 0; mutating_infix "+" $n 3; diff --git a/src/plugins/ops.ts b/src/plugins/ops.ts index f2cbf05d..e94f84ac 100644 --- a/src/plugins/ops.ts +++ b/src/plugins/ops.ts @@ -1,16 +1,15 @@ import { type Plugin, type OpTransformOutput } from "../common/Language"; import { - add1, + succ, assignment, infix, type BinaryOpCode, type Node, - flipOpCode, type IndexCall, indexCall, isBinary, isCommutative, - isIntLiteral, + isInt, isNegative, mutatingInfix, isOp, @@ -19,13 +18,19 @@ import { op, prefix, type UnaryOpCode, - BinaryOpCodes, functionCall, propertyCall, isIdent, postfix, + type VariadicOpCode, + BinaryOpCodes, + VariadicOpCodes, + isUnary, + flippedOpCode, + isVariadic, + list, + type MutatingInfix, } from "../IR"; -import { getType } from "../common/getType"; import { type Spine } from "../common/Spine"; import { stringify } from "../common/stringify"; import { mapObjectValues } from "../common/arrays"; @@ -36,22 +41,13 @@ export function mapOps( ): Plugin { return { name, + bakeType: true, visit(node, spine) { if (isOp()(node)) { const op = node.op; const f = opMap[op]; if (f !== undefined) { - let replacement = - typeof f === "function" ? f(node.args, spine as Spine) : f; - if (replacement === undefined) return undefined; - if ("op" in replacement && !isOp()(replacement)) { - // "as any" because TS doesn't do well with the "in" keyword - replacement = { - ...(replacement as any), - op: node.op, - }; - } - return { ...replacement!, type: getType(node, spine) }; + return typeof f === "function" ? f(node.args, spine as Spine) : f; } } }, @@ -95,21 +91,28 @@ export function mapTo( * Plugin transforming binary and unary ops to the name and precedence in the target lang. * @param opMap OpCode - target op name pairs. * @param asMutatingInfix - array of target op names that should be mapped to mutating infix or true for to signify all. + * @param unaryMapping - The function to transform unary ops. + * @param binaryMapping - The function to transform binary ops. * @returns The plugin closure. */ -export function mapToPrefixAndInfix< +export function mapUnaryAndBinary< TNames extends string, TNamesMutating extends TNames, >( - opMap: Partial & Record>, + opMap: Partial< + Record & Record + >, asMutatingInfix: true | TNamesMutating[] = [], + unaryMapping: (name: string, ...args: Node[]) => Node = prefix, + binaryMapping: (name: string, ...args: Node[]) => Node = infix, ): Plugin { enhanceOpMap(opMap); const justPrefixInfix = mapOps( mapObjectValues(opMap, (name, op) => - isBinary(op) - ? (x: readonly Node[]) => asBinaryChain(op, x, opMap) - : (x: readonly Node[]) => prefix(name, x[0]), + isUnary(op) + ? (x: readonly Node[]) => unaryMapping(name, x[0]) + : (x: readonly Node[]) => + asBinaryChain(op, x, opMap, unaryMapping, binaryMapping), ), `mapToPrefixAndInfix(${JSON.stringify(opMap)}, ${JSON.stringify( asMutatingInfix, @@ -121,7 +124,7 @@ export function mapToPrefixAndInfix< Object.fromEntries( Object.entries(opMap).filter( ([k, v]) => - isBinary(k as OpCode) && + (isBinary(k as OpCode) || isVariadic(k as OpCode)) && (asMutatingInfix === true || asMutatingInfix.includes(v as any)), ), ), @@ -138,17 +141,15 @@ export function mapToPrefixAndInfix< } function asBinaryChain( - opCode: BinaryOpCode, + opCode: BinaryOpCode | VariadicOpCode, exprs: readonly Node[], names: Partial>, + unaryMapping: (name: string, ...args: Node[]) => Node = prefix, + binaryMapping: (name: string, ...args: Node[]) => Node = infix, ): Node { const negName = names.neg; - if ( - opCode === "mul" && - isIntLiteral(-1n)(exprs[0]) && - negName !== undefined - ) { - exprs = [prefix(negName, exprs[1]), ...exprs.slice(2)]; + if (opCode === "mul" && isInt(-1n)(exprs[0]) && negName !== undefined) { + exprs = [unaryMapping(negName, exprs[1]), ...exprs.slice(2)]; } if (opCode === "add") { exprs = exprs @@ -159,9 +160,12 @@ function asBinaryChain( for (const expr of exprs.slice(1)) { const subName = names.sub; if (opCode === "add" && isNegative(expr) && subName !== undefined) { - result = infix(subName, result, op("neg", expr)); + result = binaryMapping(subName, result, { + ...op.neg(expr), + targetType: expr.targetType, + }); } else { - result = infix(names[opCode] ?? "?", result, expr); + result = binaryMapping(names[opCode] ?? "?", result, expr); } } return result; @@ -169,50 +173,131 @@ function asBinaryChain( export function useIndexCalls( oneIndexed: boolean = false, - ops: OpCode[] = [ - "array_get", - "list_get", - "table_get", - "array_set", - "list_set", - "table_set", + ops = [ + "at[Array]" as const, + "at[List]" as const, + "at_back[List]" as const, + "at[Table]" as const, + "set_at[Array]" as const, + "set_at[List]" as const, + "set_at_back[List]" as const, + "set_at[Table]" as const, ], ): Plugin { return { + bakeType: true, name: `useIndexCalls(${JSON.stringify(oneIndexed)}, ${JSON.stringify( ops, )})`, visit(node) { if ( isOp(...ops)(node) && - (isIdent()(node.args[0]) || node.op.endsWith("_get")) + (isIdent()(node.args[0]) || !node.op.startsWith("set_")) ) { let indexNode: IndexCall; - if (oneIndexed && !node.op.startsWith("table_")) { - indexNode = indexCall(node.args[0], add1(node.args[1]), true); + if (oneIndexed && !node.op.endsWith("[Table]")) { + indexNode = indexCall(node.args[0], succ(node.args[1])); } else { indexNode = indexCall(node.args[0], node.args[1]); } - if (node.op.endsWith("_get")) { - return indexNode; - } else if (node.op.endsWith("_set")) { + if ( + isOp( + "set_at[Array]", + "set_at[List]", + "set_at_back[List]", + "set_at[Table]", + )(node) + ) { return assignment(indexNode, node.args[2]); + } else { + return indexNode; } } }, }; } +export function backwardsIndexToForwards( + addLength = true, + ops: OpCode[] = [ + "at_back[Ascii]" as const, + "at_back[byte]" as const, + "at_back[codepoint]" as const, + "at_back[List]" as const, + "set_at_back[List]" as const, + "slice_back[Ascii]" as const, + "slice_back[byte]" as const, + "slice_back[codepoint]" as const, + "slice_back[List]" as const, + ], +): Plugin { + return { + name: "backwardsIndexToForwards", + visit(node, spine, context) { + if (isOp(...ops)(node)) { + const [collection, index, third] = node.args; + return mapOps({ + "at_back[Ascii]": op["at[Ascii]"]( + collection, + addLength ? op.add(index, op["size[Ascii]"](collection)) : index, + ), + "at_back[byte]": op["at[byte]"]( + collection, + addLength ? op.add(index, op["size[byte]"](collection)) : index, + ), + "at_back[codepoint]": op["at[codepoint]"]( + collection, + addLength + ? op.add(index, op["size[codepoint]"](collection)) + : index, + ), + "at_back[List]": op["at[List]"]( + collection, + addLength ? op.add(index, op["size[List]"](collection)) : index, + ), + "set_at_back[List]": op["set_at[List]"]( + collection, + addLength ? op.add(index, op["size[List]"](collection)) : index, + third, + ), + "slice_back[Ascii]": op["slice[Ascii]"]( + collection, + addLength ? op.add(index, op["size[Ascii]"](collection)) : index, + third, + ), + "slice_back[byte]": op["slice[byte]"]( + collection, + addLength ? op.add(index, op["size[byte]"](collection)) : index, + third, + ), + "slice_back[codepoint]": op["slice[codepoint]"]( + collection, + addLength + ? op.add(index, op["size[codepoint]"](collection)) + : index, + third, + ), + "slice_back[List]": op["slice[List]"]( + collection, + addLength ? op.add(index, op["size[List]"](collection)) : index, + third, + ), + }).visit(node, spine, context); + } + }, + }; +} + // "a = a + b" --> "a += b" export function addMutatingInfix( - opMap: Partial>, + opMap: Partial>, ): Plugin { return { name: `addMutatingInfix(${JSON.stringify(opMap)})`, visit(node) { if ( node.kind === "Assignment" && - isOp(...BinaryOpCodes)(node.expr) && + isOp(...BinaryOpCodes, ...VariadicOpCodes)(node.expr) && node.expr.args.length > 1 && node.expr.op in opMap ) { @@ -229,13 +314,13 @@ export function addMutatingInfix( return mutatingInfix( opMap.sub!, node.variable, - op("neg", op(opCode, ...newArgs)), + op.neg(op.unsafe(opCode, ...newArgs)), ); } return mutatingInfix( name, node.variable, - newArgs.length > 1 ? op(opCode, ...newArgs) : newArgs[0], + newArgs.length > 1 ? op.unsafe(opCode, ...newArgs) : newArgs[0], ); } } @@ -243,43 +328,57 @@ export function addMutatingInfix( }; } -export const addPostfixIncAndDec: Plugin = { - name: "addPostfixIncAndDec", - visit(node) { - if ( - node.kind === "MutatingInfix" && - ["+", "-"].includes(node.name) && - isIntLiteral(1n)(node.right) - ) { - return postfix(node.name.repeat(2), node.variable); - } - }, -}; +export function addIncAndDec( + transform: (infix: MutatingInfix) => Node = (x) => + postfix(x.name.repeat(2), x.variable), +): Plugin { + return { + name: `addIncAndDec(${JSON.stringify(transform)})`, + visit(node) { + if ( + node.kind === "MutatingInfix" && + ["+", "-"].includes(node.name) && + isInt(1n)(node.right) + ) { + return transform(node); + } + }, + }; +} // (a > b) --> (b < a) -export const flipBinaryOps: Plugin = { - name: "flipBinaryOps", - visit(node) { - if (isOp(...BinaryOpCodes)(node)) { - const flippedOpCode = flipOpCode(node.op); - if (flippedOpCode !== null) { - return op(flippedOpCode, node.args[1], node.args[0]); - } +export function flipBinaryOps(node: Node) { + if (isOp(...BinaryOpCodes)(node)) { + if (node.op in flippedOpCode) { + return op.unsafe( + flippedOpCode[node.op as keyof typeof flippedOpCode], + node.args[1], + node.args[0], + ); } - }, -}; + if (isCommutative(node.op)) { + return op[node.op](node.args[1], node.args[0]); + } + } +} export const removeImplicitConversions: Plugin = { name: "removeImplicitConversions", + bakeType: true, visit(node) { if (node.kind === "ImplicitConversion") { - return node.expr; + let ret: Node = node; + while (ret.kind === "ImplicitConversion") { + ret = ret.expr; + } + return ret; } }, }; export const methodsAsFunctions: Plugin = { name: "methodsAsFunctions", + bakeType: true, visit(node) { if (node.kind === "MethodCall") { return functionCall(propertyCall(node.object, node.ident), node.args); @@ -289,8 +388,24 @@ export const methodsAsFunctions: Plugin = { export const printIntToPrint: Plugin = mapOps( { - print_int: (x) => op("print", op("int_to_text", ...x)), - println_int: (x) => op("println", op("int_to_text", ...x)), + "print[Int]": (x) => op["print[Text]"](op.int_to_dec(x[0])), + "println[Int]": (x) => op["println[Text]"](op.int_to_dec(x[0])), }, "printIntToPrint", ); + +export const arraysToLists: Plugin = { + name: "arraysToLists", + bakeType: true, + visit(node) { + if (node.kind === "Array") { + return list(node.exprs); + } + if (node.kind === "Op") { + if (isOp("at[Array]")(node)) return op["at[List]"](...node.args); + if (isOp("set_at[Array]")(node)) return op["set_at[List]"](...node.args); + if (isOp("contains[Array]")(node)) + return op["contains[List]"](...node.args); + } + }, +}; diff --git a/src/plugins/packing.ts b/src/plugins/packing.ts index 8844d6b8..4aabb37b 100644 --- a/src/plugins/packing.ts +++ b/src/plugins/packing.ts @@ -1,5 +1,5 @@ -import { type Plugin } from "../common/Language"; import { + type Node, assignment, block, forRangeCommon, @@ -11,51 +11,44 @@ import { print, text, } from "../IR"; -import { byteLength } from "../common/objective"; +import { byteLength } from "../common/strings"; +import type { Spine } from "../common/Spine"; -export const useDecimalConstantPackedPrinter: Plugin = { - name: "useDecimalConstantPackedPrinter", - visit(node) { - if ( - isOp("print", "println")(node) && - isText()(node.args[0]) && - isLargeDecimalConstant(node.args[0].value) - ) { - const [prefix, main] = node.args[0].value.replace(".", ".,").split(","); - const packed = packDecimal(main); - return block([ - assignment("result", text(prefix)), - forRangeCommon( - ["packindex", 0, packed.length], - assignment( - "result", - op( - "concat", - id("result"), - op( - "text_get_byte_slice", - op( - "int_to_text", - op( - "add", - int(72n), - op( - "text_byte_to_int", - op("text_get_byte", text(packed), id("packindex")), - ), +export function useDecimalConstantPackedPrinter(node: Node, spine: Spine) { + if ( + isOp("print[Text]", "println[Text]")(node) && + isText()(node.args[0]) && + isLargeDecimalConstant(node.args[0].value) + ) { + const [prefix, main] = node.args[0].value.replace(".", ".,").split(","); + const packed = packDecimal(main); + return block([ + assignment("result", text(prefix)), + forRangeCommon( + ["packindex", 0, packed.length], + assignment( + "result", + op["concat[Text]"]( + id("result"), + op["slice[byte]"]( + op.int_to_dec( + op.add( + int(72n), + op["ord[byte]"]( + op["at[byte]"](text(packed), id("packindex")), ), ), - int(1n), - int(2n), ), + int(1n), + int(2n), ), ), ), - print(id("result")), - ]); - } - }, -}; + ), + print(id("result")), + ]); + } +} function isLargeDecimalConstant(output: string): boolean { return /^\d\.\d*$/.test(output) && output.length > 200; @@ -68,19 +61,16 @@ function packDecimal(decimal: string): string { return result; } -export const useLowDecimalListPackedPrinter: Plugin = { - name: "useLowDecimalListPackedPrinter", - visit(node) { - if (isOp("print", "println")(node) && isText()(node.args[0])) { - const packed = packLowDecimalList(node.args[0].value); - if (packed === null) return; - return forRangeCommon( - ["packindex", 0, packed.length], - print(op("text_get_byte_to_int", text(packed), id("packindex"))), - ); - } - }, -}; +export function useLowDecimalListPackedPrinter(node: Node) { + if (isOp("print[Text]", "println[Text]")(node) && isText()(node.args[0])) { + const packed = packLowDecimalList(node.args[0].value); + if (packed === null) return; + return forRangeCommon( + ["packindex", 0, packed.length], + print(op["ord_at[byte]"](text(packed), id("packindex"))), + ); + } +} function packLowDecimalList(value: string): string | null { if (/^[\d+\n]+[\d+]$/.test(value)) { diff --git a/src/plugins/print.test.md b/src/plugins/print.test.md index e8bd24e7..32e152c2 100644 --- a/src/plugins/print.test.md +++ b/src/plugins/print.test.md @@ -29,3 +29,59 @@ print "y"; println "x"; println "y"; ``` + +```polygolf +println 1; +print 2; +``` + +```polygolf print.golfLastPrintInt(true) +println 1; +println 2; +``` + +```polygolf +println 1; +println 2; +``` + +```polygolf print.golfLastPrintInt(false) +println 1; +print 2; +``` + +```polygolf +for $i 10 { + print "x"; +}; +print "y"; +``` + +```polygolf print.mergePrint +(id "unique#0") <- ""; +for $i 10 ( + (id "unique#0") <- ((id "unique#0") .. "x") +); +(id "unique#0") <- ((id "unique#0") .. "y"); +print (id "unique#0"); +``` + +```polygolf +if true { + $x <- ""; + for $i 10 { + $x <- ($x .. "x"); + }; + $x <- ($x .. "--"); + print $x; +}; +``` + +```polygolf print.splitPrint +if true { + for $i 10 ( + print "x" + ); + print "--"; +}; +``` diff --git a/src/plugins/print.ts b/src/plugins/print.ts index f954fa6e..a24e9d37 100644 --- a/src/plugins/print.ts +++ b/src/plugins/print.ts @@ -1,11 +1,31 @@ +import type { Spine } from "../common/Spine"; import { replaceAtIndex } from "../common/arrays"; import { type Plugin } from "../common/Language"; -import { block, implicitConversion, isOp, op, text } from "../IR"; +import { + block, + implicitConversion, + isOp, + type Node, + op, + text, + id, + assignment, + isUserIdent, + isAssignmentToIdent, + type Assignment, + isIdent, + blockOrSingle, + type Op, + isText, +} from "../IR"; import { mapOps } from "./ops"; +import type { VisitorContext } from "../common/compile"; +import { getWrites } from "../common/symbols"; export const printLnToPrint = mapOps( { - println: (x) => op("print", op("concat", x[0], text("\n"))), + "println[Text]": (x) => + op["print[Text]"](op["concat[Text]"](x[0], text("\n"))), }, "printLnToPrint", ); @@ -17,37 +37,142 @@ export const printLnToPrint = mapOps( export function golfLastPrint(toPrintln = true): Plugin { return { name: "golfLastPrint", - visit(program, spine) { - if (!spine.isRoot) return; - const newOp = toPrintln ? ("println" as const) : ("print" as const); - const oldOp = toPrintln ? "print" : "println"; - if (isOp(oldOp)(program)) { - return { ...program, op: newOp }; - } else if (program.kind === "Block") { - const oldChildren = program.children; - const lastStatement = oldChildren[oldChildren.length - 1]; - if (isOp(oldOp)(lastStatement)) { - const newLastStatement = { ...lastStatement, op: newOp }; - const children = replaceAtIndex( - oldChildren, - oldChildren.length - 1, - newLastStatement, + visit(program, spine, context) { + context.skipChildren(); + const statements = block([program]).children; + const newOp = toPrintln ? "println[Text]" : "print[Text]"; + const oldOp = toPrintln ? "print[Text]" : "println[Text]"; + const lastStatement = statements[statements.length - 1]; + if (isOp(oldOp, newOp)(lastStatement)) { + let arg = lastStatement.args[0]; + if (isText()(arg)) { + const value = arg.value.trimEnd(); + if (value !== arg.value) arg = text(value); + } + if (arg !== lastStatement.args[0] || lastStatement.op !== newOp) { + return blockOrSingle( + replaceAtIndex(statements, statements.length - 1, op[newOp](arg)), ); - return block(children); } } }, }; } -export const implicitlyConvertPrintArg: Plugin = { - name: "implicitlyConvertPrintArg", - visit(node, spine) { - if ( - isOp("int_to_text")(node) && - isOp("print", "println")(spine.parent!.node) - ) { - return implicitConversion(node.op, node.args[0]); - } +/** + * Like golfLastPrint but for print[Int] instead of print[Text] + */ +export function golfLastPrintInt(toPrintlnInt = true): Plugin { + return { + name: "golfLastPrintInt", + visit(program, spine, context) { + context.skipChildren(); + const statements = block([program]).children; + const newOp = toPrintlnInt ? "println[Int]" : "print[Int]"; + const oldOp = toPrintlnInt ? "print[Int]" : "println[Int]"; + const lastStatement = statements[statements.length - 1]; + if (isOp(oldOp)(lastStatement)) { + return blockOrSingle( + replaceAtIndex( + statements, + statements.length - 1, + op[newOp](lastStatement.args[0]), + ), + ); + } + }, + }; +} + +export function implicitlyConvertPrintArg(node: Node, spine: Spine) { + if ( + isOp("int_to_dec")(node) && + !spine.isRoot && + isOp("print[Text]", "println[Text]")(spine.parent!.node) + ) { + return implicitConversion(node.op, node.args[0]); + } +} + +export const printToImplicitOutput = mapOps( + { + "print[Text]": (x) => x[0], }, -}; + "printToImplicitOutput", +); + +export function printConcatToMultiPrint(node: Node, spine: Spine) { + if (isOp("print[Text]")(node) && isOp("concat[Text]")(node.args[0])) { + return block(node.args[0].args.map(op["print[Text]"])); + } +} + +export const putcToPrintChar = mapOps( + { + "putc[Ascii]": (x) => op["print[Text]"](op["char[Ascii]"](x[0])), + "putc[byte]": (x) => op["print[Text]"](op["char[byte]"](x[0])), + "putc[codepoint]": (x) => op["print[Text]"](op["char[codepoint]"](x[0])), + }, + "putcToPrintChar", +); + +export function mergePrint( + program: Node, + spine: Spine, + context: VisitorContext, +) { + context.skipChildren(); + const variable = id(); + if (spine.countNodes(isOp("print[Text]", "println[Text]")) > 1) { + const newSpine = spine.withReplacer((node) => + isOp("print[Text]", "println[Text]")(node) + ? assignment( + variable, + op["concat[Text]"]( + variable, + node.args[0], + ...(node.op === "print[Text]" ? [] : [text("\n")]), + ), + ) + : undefined, + ); + return block([ + assignment(variable, text("")), + newSpine.node, + op["print[Text]"](variable), + ]); + } +} + +export function splitPrint(node: Node, spine: Spine) { + if (node.kind === "Block") { + const last = node.children.at(-1)!; + if (isOp("print[Text]")(last) && isUserIdent()(last.args[0])) { + const printVar = last.args[0]; + const writes = getWrites(spine, printVar.name); + if (writes.every((x) => isAssignmentToIdent()(x.parent!.node))) { + const assignments = writes.map((x) => x.parent?.node as Assignment); + if ( + assignments.every( + (x, i) => + i < 1 || + (isOp("concat[Text]")(x.expr) && + isIdent(printVar)(x.expr.args[0])), + ) + ) { + return spine.withReplacer((x) => + x === node + ? blockOrSingle(node.children.slice(0, -1)) + : x === assignments[0] + ? isText("")(x.expr) + ? block([]) + : op["print[Text]"](x.expr) + : assignments.includes(x as any) + ? op["print[Text]"](((x as Assignment).expr as Op).args[1]!) + : undefined, + ).node; + } + } + } + } +} diff --git a/src/plugins/static.test.md b/src/plugins/static.test.md index 48e99727..12bce381 100644 --- a/src/plugins/static.test.md +++ b/src/plugins/static.test.md @@ -22,10 +22,10 @@ text_split "a!b!c d" "!"; ```polygolf list_get (list "a" "b" "c") 2; -list_find (list "a" "b" "c") 2; +list_find (list "a" "b" "c") "b"; ``` -```polygolf static.listOpsToTextOps("text_get_codepoint","text_codepoint_find") +```polygolf static.listOpsToTextOps("at[codepoint]","find[codepoint]") text_get_codepoint "abc" 2; -text_codepoint_find "abc" 2; +text_codepoint_find "abc" "b"; ``` diff --git a/src/plugins/static.ts b/src/plugins/static.ts index 079bca7e..d2addbad 100644 --- a/src/plugins/static.ts +++ b/src/plugins/static.ts @@ -1,6 +1,7 @@ +import { getOutput } from "../interpreter"; import { isOp, op, text, isText } from "../IR"; import { type Plugin } from "../common/Language"; -import { byteLength, charLength } from "../common/objective"; +import { byteLength, charLength } from "../common/strings"; export function golfStringListLiteral(useTextSplitWhitespace = true): Plugin { return { @@ -10,8 +11,8 @@ export function golfStringListLiteral(useTextSplitWhitespace = true): Plugin { const strings = node.exprs.map((x) => x.value); const delim = getDelim(strings, useTextSplitWhitespace); return delim === true - ? op("text_split_whitespace", text(strings.join(" "))) - : op("text_split", text(strings.join(delim)), text(delim)); + ? op.split_whitespace(text(strings.join(" "))) + : op.split(text(strings.join(delim)), text(delim)); } }, }; @@ -37,27 +38,17 @@ function getDelim( } export function listOpsToTextOps( - ...ops: ( - | "text_get_byte" - | "text_get_codepoint" - | "text_byte_find" - | "text_codepoint_find" - )[] + ...ops: ("at[byte]" | "at[codepoint]" | "find[byte]" | "find[codepoint]")[] ): Plugin { ops = ops.length > 0 ? ops - : [ - "text_get_byte", - "text_get_codepoint", - "text_byte_find", - "text_codepoint_find", - ]; + : ["at[byte]", "at[codepoint]", "find[byte]", "find[codepoint]"]; return { name: `listOpsToTextOps(${JSON.stringify(ops)})`, visit(node) { if ( - isOp("list_get", "list_find")(node) && + isOp("at[List]", "find[List]")(node) && node.args[0].kind === "List" && node.args[0].exprs.every(isText()) ) { @@ -65,17 +56,32 @@ export function listOpsToTextOps( if (texts.every((x) => charLength(x) === 1)) { const joined = text(texts.join("")); if (texts.every((x) => byteLength(x) === 1)) { - if (node.op === "list_get" && ops.includes("text_get_byte")) - return op("text_get_byte", joined, node.args[1]); - if (node.op === "list_find" && ops.includes("text_byte_find")) - return op("text_byte_find", joined, node.args[1]); + if (node.op === "at[List]" && ops.includes("at[byte]")) + return op["at[byte]"](joined, node.args[1]); + if (node.op === "find[List]" && ops.includes("find[byte]")) + return op["find[byte]"](joined, node.args[1]); } - if (node.op === "list_get" && ops.includes("text_get_codepoint")) - return op("text_get_codepoint", joined, node.args[1]); - if (node.op === "list_find" && ops.includes("text_codepoint_find")) - return op("text_codepoint_find", joined, node.args[1]); + if (node.op === "at[List]" && ops.includes("at[codepoint]")) + return op["at[codepoint]"](joined, node.args[1]); + if (node.op === "find[List]" && ops.includes("find[codepoint]")) + return op["find[codepoint]"](joined, node.args[1]); } } }, }; } + +export function hardcode(): Plugin { + return { + name: "hardcode", + visit(node, spine, context) { + context.skipChildren(); + if (!isOp("print[Text]")(node) || !isText()(node.args[0])) { + try { + const output = getOutput(node); + if (output !== "") return op["print[Text]"](text(output)); + } catch {} + } + }, + }; +} diff --git a/src/plugins/tables.test.md b/src/plugins/tables.test.md index b70e4598..ea46f87f 100644 --- a/src/plugins/tables.test.md +++ b/src/plugins/tables.test.md @@ -3,22 +3,22 @@ ## Hashing ```polygolf -print (table_get +print[Text] (table_get (table ("▄ ▄▄▄" => "A") ("▄▄▄ ▄ ▄ ▄" => "B") ("▄▄▄ ▄ ▄▄▄ ▄" => "C") ) - (argv_get 0) + (at[argv] 0) ); ``` ```polygolf tables.testTableHashing(999) -print (list_get (list "B" "C" "A") (((function_call (builtin "hash") (argv_get 0)):0..4294967295 mod 11) mod 3)); +print (list_get (list "B" "C" "A") (((function_call (builtin "hash") (at[argv] 0)):0..4294967295 mod 11) mod 3)); ``` ```polygolf tables.testTableHashing(9) -print (list_get (list "A" "B" " " "C") (((function_call (builtin "hash") (argv_get 0)):0..4294967295 mod 9) mod 4)); +print (list_get (list "A" "B" " " "C") (((function_call (builtin "hash") (at[argv] 0)):0..4294967295 mod 9) mod 4)); ``` ## List lookup @@ -30,10 +30,10 @@ table_get ("▄▄▄ ▄ ▄ ▄" => "B") ("▄▄▄ ▄ ▄▄▄ ▄" => "C") ) - (argv_get 0) + (at[argv] 0) ; ``` ```polygolf tables.tableToListLookup -list_get (list "A" "B" "C") (list_find (list "▄ ▄▄▄" "▄▄▄ ▄ ▄ ▄" "▄▄▄ ▄ ▄▄▄ ▄") (argv_get 0)); +list_get (list "A" "B" "C") (list_find (list "▄ ▄▄▄" "▄▄▄ ▄ ▄ ▄" "▄▄▄ ▄ ▄▄▄ ▄") (at[argv] 0)); ``` diff --git a/src/plugins/tables.ts b/src/plugins/tables.ts index b177c1af..0e86b200 100644 --- a/src/plugins/tables.ts +++ b/src/plugins/tables.ts @@ -39,7 +39,7 @@ export function tableHashing( return { name: "tableHashing(...)", visit(node, spine) { - if (isOp("table_get")(node) && node.args[0].kind === "Table") { + if (isOp("at[Table]")(node) && node.args[0].kind === "Table") { const table = node.args[0]; const getKey = node.args[1]; const tableType = getType(table, spine); @@ -58,18 +58,16 @@ export function tableHashing( let lastUsed = array.length - 1; while (array[lastUsed] === null) lastUsed--; - return op( - "list_get", + return op["at[List]"]( list( array .slice(0, lastUsed + 1) .map((x) => x ?? defaultValue(tableType.value)), ), - op( - "mod", + op.mod( mod === array.length ? hash(getKey) - : op("mod", hash(getKey), int(mod)), + : op.mod(hash(getKey), int(mod)), int(array.length), ), ); @@ -127,19 +125,16 @@ export function testTableHashing(maxMod: number): Plugin { }; } -export const tableToListLookup: Plugin = { - name: "tableToListLookup", - visit(node) { - if (isOp("table_get")(node) && node.args[0].kind === "Table") { - const keys = node.args[0].kvPairs.map((x) => x.key); - if ( - keys.every(isOfKind("Integer", "Text")) && - new Set(keys.map((x) => x.value)).size === keys.length - ) { - const values = node.args[0].kvPairs.map((x) => x.value); - const at = node.args[1]; - return op("list_get", list(values), op("list_find", list(keys), at)); - } +export function tableToListLookup(node: Node) { + if (isOp("at[Table]")(node) && node.args[0].kind === "Table") { + const keys = node.args[0].kvPairs.map((x) => x.key); + if ( + keys.every(isOfKind("Integer", "Text")) && + new Set(keys.map((x) => x.value)).size === keys.length + ) { + const values = node.args[0].kvPairs.map((x) => x.value); + const at = node.args[1]; + return op["at[List]"](list(values), op["find[List]"](list(keys), at)); } - }, -}; + } +} diff --git a/src/plugins/textOps.test.md b/src/plugins/textOps.test.md index 5f775d89..c4ea3eac 100644 --- a/src/plugins/textOps.test.md +++ b/src/plugins/textOps.test.md @@ -1,38 +1,6 @@ # Text ops plugins -## Text ops equivalent for ascii - -```polygolf -text_byte_length "abcdefgh"; -text_get_byte_slice "abcdefgh" 1 3; -text_byte_length "ěščřžýáíé"; -int_to_text_byte 48; -int_to_text_byte 150; -``` - -```polygolf textOps.useEquivalentTextOp(true,true) -text_codepoint_length "abcdefgh"; -text_get_codepoint_slice "abcdefgh" 1 3; -text_byte_length "ěščřžýáíé"; -int_to_codepoint 48; -int_to_text_byte 150; -``` - -```polygolf -text_codepoint_length "abcdefgh"; -text_get_codepoint_slice "abcdefgh" 1 3; -text_codepoint_length "ěščřžýáíé"; -int_to_codepoint 48; -int_to_codepoint 150; -``` - -```polygolf textOps.useEquivalentTextOp(true,true) -text_byte_length "abcdefgh"; -text_get_byte_slice "abcdefgh" 1 3; -text_codepoint_length "ěščřžýáíé"; -int_to_text_byte 48; -int_to_codepoint 150; -``` +## Replace ```polygolf text_replace (text_replace "ABCD" "A" "a") "BC" "b"; @@ -53,3 +21,29 @@ text_replace (text_replace "ABCD" "A" "a") "BC" "b"; ```polygolf textOps.useMultireplace(true) text_replace (text_replace "ABCD" "A" "a") "BC" "b"; ``` + +## Starts/ends with + +```polygolf +$a <- (@0); +$b <- (@1); +starts_with $a $b; +``` + +```polygolf textOps.startsWithEndsWithToSliceEquality("byte") +$a <- (@0); +$b <- (@1); +(slice[byte] $a 0 (size[byte] $b)) == $b; +``` + +```polygolf +$a <- (@0); +$b <- (@1); +ends_with $a $b; +``` + +```polygolf textOps.startsWithEndsWithToSliceEquality("codepoint") +$a <- (@0); +$b <- (@1); +(slice_back[codepoint] $a (- (size[codepoint] $b)) (size[codepoint] $b)) == $b; +``` diff --git a/src/plugins/textOps.ts b/src/plugins/textOps.ts index 85874f60..e1697805 100644 --- a/src/plugins/textOps.ts +++ b/src/plugins/textOps.ts @@ -1,62 +1,17 @@ -import { getType } from "../common/getType"; -import { - integerType, - isOp, - isSubtype, - isText, - type OpCode, - op, - int, -} from "../IR"; +import { isOp, isText, op, int, isOpCode } from "../IR"; import { type Plugin } from "../common/Language"; import { mapOps } from "./ops"; -import { charLength } from "../common/objective"; +import { charLength } from "../common/strings"; -function toBidirectionalMap(pairs: [T, T][]): Map { - return new Map([...pairs, ...pairs.map<[T, T]>(([k, v]) => [v, k])]); -} - -const textOpsEquivalenceAscii = toBidirectionalMap([ - ["text_codepoint_find", "text_byte_find"], - ["text_get_codepoint", "text_get_byte"], - ["text_get_codepoint_to_int", "text_get_byte_to_int"], - ["text_codepoint_length", "text_byte_length"], - ["text_codepoint_reversed", "text_byte_reversed"], - ["text_get_codepoint_slice", "text_get_byte_slice"], - ["codepoint_to_int", "text_byte_to_int"], -]); - -const integerOpsEquivalenceAscii = toBidirectionalMap([ - ["int_to_text_byte", "int_to_codepoint"], -]); - -/** Swaps an op to another one, provided they are equivalent for the subtype. */ -export function useEquivalentTextOp( - useBytes = true, - useCodepoints = true, -): Plugin { - if (!useBytes && !useCodepoints) - throw new Error( - "Programming error. Choose at least one of bytes and codepoints.", - ); +/** Implements ascii text op by either byte / codepoint text ops. */ +export function usePrimaryTextOps(char: "byte" | "codepoint"): Plugin { return { - name: `useEquivalentTextOp(${useBytes.toString()}, ${useCodepoints.toString()})`, - visit(node, spine) { - if (!isOp()(node)) return; - if (node.args.length < 1) return; - const typeArg0 = getType(node.args[0], spine); - if ( - (!useBytes && node.op.includes("codepoint")) || - (!useCodepoints && node.op.includes("byte")) - ) - return; - if (typeArg0.kind === "text" && typeArg0.isAscii) { - const alternative = textOpsEquivalenceAscii.get(node.op); - if (alternative !== undefined) return { ...node, op: alternative }; - } - if (isSubtype(typeArg0, integerType(0, 127))) { - const alternative = integerOpsEquivalenceAscii.get(node.op); - if (alternative !== undefined) return { ...node, op: alternative }; + name: `usePrimaryTextOps(${JSON.stringify(char)})`, + visit(node) { + if (!isOp()(node) || !node.op.includes("[Ascii]")) return; + const replacement = node.op.replace("[Ascii]", `[${char}]`); + if (isOpCode(replacement)) { + return op.unsafe(replacement, ...node.args); } }, }; @@ -64,23 +19,27 @@ export function useEquivalentTextOp( export const textGetToIntToTextGet: Plugin = mapOps( { - text_get_byte_to_int: (x) => - op("text_byte_to_int", op("text_get_byte", ...x)), - text_get_codepoint_to_int: (x) => - op("codepoint_to_int", op("text_get_codepoint", ...x)), + "ord_at[Ascii]": (x) => op["ord[Ascii]"](op["at[Ascii]"](x[0], x[1])), + "ord_at[byte]": (x) => op["ord[byte]"](op["at[byte]"](x[0], x[1])), + "ord_at[codepoint]": (x) => + op["ord[codepoint]"](op["at[codepoint]"](x[0], x[1])), + "ord_at_back[Ascii]": (x) => + op["ord[Ascii]"](op["at_back[Ascii]"](x[0], x[1])), + "ord_at_back[byte]": (x) => + op["ord[byte]"](op["at_back[byte]"](x[0], x[1])), + "ord_at_back[codepoint]": (x) => + op["ord[codepoint]"](op["at_back[codepoint]"](x[0], x[1])), }, "textGetToIntToTextGet", ); export const textToIntToTextGetToInt: Plugin = mapOps( { - text_byte_to_int: (x) => - isOp("text_get_byte")(x[0]) - ? op("text_get_byte_to_int", ...x[0].args) - : undefined, - codepoint_to_int: (x) => - isOp("text_get_codepoint")(x[0]) - ? op("text_get_codepoint_to_int", ...x[0].args) + "ord[byte]": (x) => + isOp("at[byte]")(x[0]) ? op["ord_at[byte]"](...x[0].args) : undefined, + "ord[codepoint]": (x) => + isOp("at[codepoint]")(x[0]) + ? op["ord_at[codepoint]"](...x[0].args) : undefined, }, "textToIntToTextGetToInt", @@ -88,18 +47,18 @@ export const textToIntToTextGetToInt: Plugin = mapOps( export const textGetToTextGetToIntToText: Plugin = mapOps( { - text_get_byte: (x) => - op("int_to_text_byte", op("text_get_byte_to_int", ...x)), - text_get_codepoint: (x) => - op("int_to_codepoint", op("text_get_codepoint_to_int", ...x)), + "at[byte]": (x) => op["char[byte]"](op["ord_at[byte]"](x[0], x[1])), + "at[codepoint]": (x) => + op["char[codepoint]"](op["ord_at[codepoint]"](x[0], x[1])), }, "textGetToTextGetToIntToText", ); export const textToIntToFirstIndexTextGetToInt: Plugin = mapOps( { - text_byte_to_int: (x) => op("text_get_byte_to_int", x[0], int(0n)), - codepoint_to_int: (x) => op("text_get_codepoint_to_int", x[0], int(0n)), + "ord[Ascii]": (x) => op["ord_at[Ascii]"](x[0], int(0n)), + "ord[byte]": (x) => op["ord_at[byte]"](x[0], int(0n)), + "ord[codepoint]": (x) => op["ord_at[codepoint]"](x[0], int(0n)), }, "textToIntToFirstIndexTextGetToInt", ); @@ -115,7 +74,7 @@ export function useMultireplace(singleCharInputsOnly = false): Plugin { return { name: "useMultireplace", visit(node) { - const isReplace = isOp("text_replace", "text_multireplace"); + const isReplace = isOp("replace", "text_multireplace"); if (isReplace(node) && isReplace(node.args[0])) { const a = node.args[0].args.slice(1); const b = node.args.slice(1); @@ -137,7 +96,7 @@ export function useMultireplace(singleCharInputsOnly = false): Plugin { ![...bInSet].some((x) => aOutSet.has(x)) && ![...aInSet].some((x) => bOutSet.has(x)) ) { - return op("text_multireplace", ...node.args[0].args, ...b); + return op.unsafe("text_multireplace", ...node.args[0].args, ...b); } } } @@ -147,7 +106,37 @@ export function useMultireplace(singleCharInputsOnly = false): Plugin { export const replaceToSplitAndJoin: Plugin = mapOps( { - text_replace: ([x, y, z]) => op("join", op("text_split", x, y), z), + replace: ([x, y, z]) => op.join(op.split(x, y), z), }, "replaceToSplitAndJoin", ); + +export function startsWithEndsWithToSliceEquality( + char: "byte" | "codepoint", +): Plugin { + return { + name: `startsWithEndsWithToSliceEquality(${JSON.stringify(char)})`, + visit(node) { + if (isOp("starts_with")(node)) { + return op["eq[Text]"]( + op[`slice[${char}]`]( + node.args[0], + int(0), + op[`size[${char}]`](node.args[1]), + ), + node.args[1], + ); + } + if (isOp("ends_with")(node)) { + return op["eq[Text]"]( + op[`slice_back[${char}]`]( + node.args[0], + op.neg(op[`size[${char}]`](node.args[1])), + op[`size[${char}]`](node.args[1]), + ), + node.args[1], + ); + } + }, + }; +} diff --git a/src/plugins/types.ts b/src/plugins/types.ts index bf3353ff..4a8e4082 100644 --- a/src/plugins/types.ts +++ b/src/plugins/types.ts @@ -1,3 +1,4 @@ +import type { Spine } from "../common/Spine"; import { PolygolfError } from "../common/errors"; import { getType } from "../common/getType"; import { type Plugin } from "../common/Language"; @@ -18,25 +19,63 @@ import { annotate, } from "../IR"; -export const assertInt64: Plugin = { - name: "assertInt64", - visit(node, spine) { - if (spine.isRoot) return; - let type: Type; - try { - type = getType(node, spine); - } catch { - return; // stuff like builtin identifiers etc. throw - } - if (isSubtype(type, integerType()) && !isSubtype(type, int64Type)) { - throw new PolygolfError( - `Integer value that doesn't provably fit into a int64 type encountered.`, - node.source, - ); +export function assertInt64(node: Node, spine: Spine) { + let type: Type; + try { + type = getType(node, spine); + } catch { + return; // stuff like builtin identifiers etc. throw + } + if (isSubtype(type, integerType()) && !isSubtype(type, int64Type)) { + throw new PolygolfError( + `Integer value that doesn't provably fit into a int64 type encountered.`, + node.source, + ); + } + return undefined; +} + +function needsBigint( + primitiveIntType: IntegerType, + allowed: Partial>, + node: Node, + spine: Spine, +): boolean { + const nodeType = getType(isAssignment(node) ? node.variable : node, spine); + if ( + isSubtype(nodeType, integerType()) && + !isSubtype(nodeType, primitiveIntType) && + node.targetType !== "bigint" + ) { + return true; + } + if (!spine.isRoot) { + const parent = spine.parent!.node; + if (isOp()(parent) || isAssignment(parent)) { + if ( + parent.targetType === "bigint" || + node.targetType === "bigint" || + (isSubtype(nodeType, integerType()) && + isOp()(node) && + spine + .getChildSpines() + .some((s) => needsBigint(primitiveIntType, allowed, s.node, s))) + ) { + const op = isOp()(parent) ? parent.op : "Assignment"; + const res = (allowed as any)[op]; + if (res === undefined) { + throw new PolygolfError( + `Operation that is not supported on bigints encountered. (${op})`, + ); + } + if (res === "bigint" && node.targetType !== "bigint") { + return true; + } + } } - return undefined; - }, -}; + } + return false; +} export function floodBigints( primitiveIntType0: "int64" | "int53" | IntegerType, @@ -46,35 +85,12 @@ export function floodBigints( return { name: "floodBigints", visit(node, spine) { - const nodeType = getType( - isAssignment(node) ? node.variable : node, - spine, - ); if ( - isSubtype(nodeType, integerType()) && - !isSubtype(nodeType, primitiveIntType) && + needsBigint(primitiveIntType, allowed, node, spine) && node.targetType !== "bigint" ) { return { ...node, targetType: "bigint" }; } - if (!spine.isRoot) { - const parent = spine.parent!.node; - if (isOp()(parent) || isAssignment(parent)) { - if (parent.targetType === "bigint" || node.targetType === "bigint") { - const res = (allowed as any)[ - isOp()(parent) ? parent.op : "Assignment" - ]; - if (res === undefined) { - throw new PolygolfError( - "Operation that is not supported on bigints encountered.", - ); - } - if (res === "bigint" && node.targetType !== "bigint") { - return { ...node, targetType: "bigint" }; - } - } - } - } }, }; } diff --git a/src/programs/code.golf-default.test.md b/src/programs/code.golf-default.test.md index 8b0cfac0..f1f44162 100644 --- a/src/programs/code.golf-default.test.md +++ b/src/programs/code.golf-default.test.md @@ -22,15 +22,17 @@ for_argv $arg 1000 { _Golfscript_ ```gs -:a;"Hello, World!"n 10,{:i;i n}%a{:A;A n}% +:a;"Hello, World! +"10,{:i;i n}%a{:b;b n}% ``` _Lua_ ```lua -print("Hello, World!") -for i=0,9 do print(i)end -for a=1,1e3 do print(arg[a])end +p=print +p("Hello, World!") +for i=0,9 do p(i)end +for a=1,1e3 do p(arg[a])end ``` _Nim_ @@ -38,8 +40,8 @@ _Nim_ ```nim import os echo"Hello, World!" -for i in..9:i.echo -for a in..999:(paramStr 1+a).echo +for i in..9:echo i +for a in..999:echo paramStr 1+a ``` _Python_ From bd54f68a5faa88aa2fccbe62691164acf3a8e1f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20Mar=C5=A1=C3=A1lek?= Date: Thu, 28 Dec 2023 01:07:01 +0100 Subject: [PATCH 09/10] remove tempId func --- src/common/symbols.ts | 6 ------ src/plugins/loops.ts | 3 +-- 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/src/common/symbols.ts b/src/common/symbols.ts index 16e68253..1547cbd3 100644 --- a/src/common/symbols.ts +++ b/src/common/symbols.ts @@ -352,9 +352,3 @@ export function readsFromArgv(node: Node): boolean { export function readsFromInput(node: Node): boolean { return readsFromArgv(node) || readsFromStdin(node); } - -// TODO: global counter here silly; -let globalID = 0; -export function tempId() { - return `__tmp_id_${globalID++}`; -} diff --git a/src/plugins/loops.ts b/src/plugins/loops.ts index 0db89f86..11cb008d 100644 --- a/src/plugins/loops.ts +++ b/src/plugins/loops.ts @@ -35,7 +35,6 @@ import { } from "../IR"; import { byteLength, charLength } from "../common/strings"; import { PolygolfError } from "../common/errors"; -import { tempId } from "../common/symbols"; export function forRangeToForRangeInclusive(skip1Step = false): Plugin { return { @@ -426,7 +425,7 @@ export const whileToRecursion: Plugin = { function whileToRecursionVisitor(node: IR.Node) { if (node.kind !== "While") return; - const name = id(tempId()); + const name = id(); return [ // Create a function to perform the while functionDefinition( From ca70120d5a11c687a4e06ee6f852c135b27b3fc7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20Mar=C5=A1=C3=A1lek?= Date: Thu, 28 Dec 2023 01:32:05 +0100 Subject: [PATCH 10/10] return block not array of nodes from plugin --- src/languages/tex/plugins.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/languages/tex/plugins.ts b/src/languages/tex/plugins.ts index 6f9e6e73..a1190753 100644 --- a/src/languages/tex/plugins.ts +++ b/src/languages/tex/plugins.ts @@ -33,7 +33,7 @@ export const exprTreeToFlat2AC: Plugin = { visit(node, spine) { if (spine.parent?.node.kind !== "Block") return; if (isOfKind("Assignment", "Op", "If")(node)) - return [...convertNodeToListOfStatements(node)]; + return block(convertNodeToListOfStatements(node)); }, };