Skip to content

Commit

Permalink
refactor: add utility to err on unreachable branches (#991)
Browse files Browse the repository at this point in the history
and use it in pattern matching over all key/value map type pairs
  • Loading branch information
verytactical authored Nov 11, 2024
1 parent b8c07e1 commit bb25297
Show file tree
Hide file tree
Showing 3 changed files with 164 additions and 144 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- The `SendDefaultMode` send mode constant to the standard library: PR [#1010](https://github.com/tact-lang/tact/pull/1010)
- Docs: initial semi-automated Chinese translation of the documentation: PR [#942](https://github.com/tact-lang/tact/pull/942)
- The `replace` and `replaceGet` methods for the `Map` type: PR [#941](https://github.com/tact-lang/tact/pull/941)
- Utility for logging errors in code that was supposed to be unreachable [#991](https://github.com/tact-lang/tact/pull/991)

### Changed

Expand Down
217 changes: 73 additions & 144 deletions src/generator/writers/writeStdlib.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { contractErrors } from "../../abi/errors";
import { maxTupleSize } from "../../bindings/typescript/writeStruct";
import { enabledMasterchain } from "../../config/features";
import { throwInternalCompilerError } from "../../errors";
import { match } from "../../utils/tricks";
import { WriterContext } from "../Writer";

export function writeStdlib(ctx: WriterContext): void {
Expand Down Expand Up @@ -1279,44 +1279,24 @@ function genTactDictSet(
return "idict_delete?";
}
};
const returnExpr = () => {
switch (`${key}:${value}`) {
case "int:int":
return "(idict_set_builder(d, kl, k, begin_cell().store_int(v, vl)), ())";
case "int:uint":
return "(idict_set_builder(d, kl, k, begin_cell().store_uint(v, vl)), ())";
case "int:coins":
return "(idict_set_builder(d, kl, k, begin_cell().store_coins(v)), ())";
case "uint:int":
return "(udict_set_builder(d, kl, k, begin_cell().store_int(v, vl)), ())";
case "uint:uint":
return "(udict_set_builder(d, kl, k, begin_cell().store_uint(v, vl)), ())";
case "uint:coins":
return "(udict_set_builder(d, kl, k, begin_cell().store_coins(v)), ())";
case "slice:int":
return "(dict_set_builder(d, kl, k, begin_cell().store_int(v, vl)), ())";
case "slice:uint":
return "(dict_set_builder(d, kl, k, begin_cell().store_uint(v, vl)), ())";
case "slice:coins":
return "(dict_set_builder(d, kl, k, begin_cell().store_coins(v)), ())";
case "int:cell":
return "(idict_set_ref(d, kl, k, v), ())";
case "uint:cell":
return "(udict_set_ref(d, kl, k, v), ())";
case "int:slice":
return "(idict_set(d, kl, k, v), ())";
case "uint:slice":
return "(udict_set(d, kl, k, v), ())";
case "slice:cell":
return `${ctx.used("__tact_dict_set_ref")}(d, kl, k, v)`;
case "slice:slice":
return "(dict_set_builder(d, kl, k, begin_cell().store_slice(v)), ())";
default:
throwInternalCompilerError(
`Unprocessed combination of key/value types ${key}/${value} in dict set operation`,
);
}
};
// prettier-ignore
const returnExpr = () => match(key, value)
.on("int", "int")(() => "(idict_set_builder(d, kl, k, begin_cell().store_int(v, vl)), ())")
.on("int", "uint")(() => "(idict_set_builder(d, kl, k, begin_cell().store_uint(v, vl)), ())")
.on("int", "coins")(() => "(idict_set_builder(d, kl, k, begin_cell().store_coins(v)), ())")
.on("uint", "int")(() => "(udict_set_builder(d, kl, k, begin_cell().store_int(v, vl)), ())")
.on("uint", "uint")(() => "(udict_set_builder(d, kl, k, begin_cell().store_uint(v, vl)), ())")
.on("uint", "coins")(() => "(udict_set_builder(d, kl, k, begin_cell().store_coins(v)), ())")
.on("slice", "int")(() => "(dict_set_builder(d, kl, k, begin_cell().store_int(v, vl)), ())")
.on("slice", "uint")(() => "(dict_set_builder(d, kl, k, begin_cell().store_uint(v, vl)), ())")
.on("slice", "coins")(() => "(dict_set_builder(d, kl, k, begin_cell().store_coins(v)), ())")
.on("int", "cell")(() => "(idict_set_ref(d, kl, k, v), ())")
.on("uint", "cell")(() => "(udict_set_ref(d, kl, k, v), ())")
.on("int", "slice")(() => "(idict_set(d, kl, k, v), ())")
.on("uint", "slice")(() => "(udict_set(d, kl, k, v), ())")
.on("slice", "cell")(() => `${ctx.used("__tact_dict_set_ref")}(d, kl, k, v)`)
.on("slice", "slice")(() => "(dict_set_builder(d, kl, k, begin_cell().store_slice(v)), ())")
.end();
ctx.fun(`__tact_dict_set_${key}_${value}`, () => {
ctx.signature(
`(cell, ()) __tact_dict_set_${key}_${value}(cell d, int kl, ${signatureKeyType} k, ${signatureValueType} v${valBitsArg()})`,
Expand Down Expand Up @@ -1354,35 +1334,24 @@ function genTactDictGetMin(
return ", int vl";
}
};
const dictGetMin = () => {
switch (`${key}:${value}`) {
case "int:int":
case "int:uint":
case "int:coins":
case "int:slice":
return "idict_get_min?";
case "uint:int":
case "uint:uint":
case "uint:coins":
case "uint:slice":
return "udict_get_min?";
case "slice:int":
case "slice:uint":
case "slice:coins":
case "slice:slice":
return ctx.used("__tact_dict_min");
case "int:cell":
return "idict_get_min_ref?";
case "uint:cell":
return "udict_get_min_ref?";
case "slice:cell":
return ctx.used("__tact_dict_min_ref");
default:
throwInternalCompilerError(
`Unprocessed combination of key/value types ${key}/${value} in dict get min operation`,
);
}
};
// prettier-ignore
const dictGetMin = () => match(key, value)
.on("int", "int")(() => "idict_get_min?")
.on("int", "uint")(() => "idict_get_min?")
.on("int", "coins")(() => "idict_get_min?")
.on("int", "slice")(() => "idict_get_min?")
.on("uint", "int")(() => "udict_get_min?")
.on("uint", "uint")(() => "udict_get_min?")
.on("uint", "coins")(() => "udict_get_min?")
.on("uint", "slice")(() => "udict_get_min?")
.on("slice", "int")(() => ctx.used("__tact_dict_min"))
.on("slice", "uint")(() => ctx.used("__tact_dict_min"))
.on("slice", "coins")(() => ctx.used("__tact_dict_min"))
.on("slice", "slice")(() => ctx.used("__tact_dict_min"))
.on("int", "cell")(() => "idict_get_min_ref?")
.on("uint", "cell")(() => "udict_get_min_ref?")
.on("slice", "cell")(() => ctx.used("__tact_dict_min_ref"))
.end();
const returnValExpr = () => {
switch (value) {
case "int":
Expand Down Expand Up @@ -1510,44 +1479,24 @@ function genTactDictReplace(
return "idict_delete?";
}
};
const returnExpr = () => {
switch (`${key}:${value}`) {
case "int:int":
return "idict_replace_builder?(d, kl, k, begin_cell().store_int(v, vl))";
case "int:uint":
return "idict_replace_builder?(d, kl, k, begin_cell().store_uint(v, vl))";
case "int:coins":
return "idict_replace_builder?(d, kl, k, begin_cell().store_coins(v))";
case "uint:int":
return "udict_replace_builder?(d, kl, k, begin_cell().store_int(v, vl))";
case "uint:uint":
return "udict_replace_builder?(d, kl, k, begin_cell().store_uint(v, vl))";
case "uint:coins":
return "udict_replace_builder?(d, kl, k, begin_cell().store_coins(v))";
case "slice:int":
return "dict_replace_builder?(d, kl, k, begin_cell().store_int(v, vl))";
case "slice:uint":
return "dict_replace_builder?(d, kl, k, begin_cell().store_uint(v, vl))";
case "slice:coins":
return "dict_replace_builder?(d, kl, k, begin_cell().store_coins(v))";
case "int:cell":
return "idict_replace_ref?(d, kl, k, v)";
case "uint:cell":
return "udict_replace_ref?(d, kl, k, v)";
case "int:slice":
return "idict_replace?(d, kl, k, v)";
case "uint:slice":
return "udict_replace?(d, kl, k, v)";
case "slice:cell":
return `${ctx.used("__tact_dict_replace_ref")}(d, kl, k, v)`;
case "slice:slice":
return "dict_replace_builder?(d, kl, k, begin_cell().store_slice(v))";
default:
throwInternalCompilerError(
`Unprocessed combination of key/value types ${key}/${value} in dict replace operation`,
);
}
};
// prettier-ignore
const returnExpr = () => match(key, value)
.on("int", "int")(() => "idict_replace_builder?(d, kl, k, begin_cell().store_int(v, vl))")
.on("int", "uint")(() => "idict_replace_builder?(d, kl, k, begin_cell().store_uint(v, vl))")
.on("int", "coins")(() => "idict_replace_builder?(d, kl, k, begin_cell().store_coins(v))")
.on("uint", "int")(() => "udict_replace_builder?(d, kl, k, begin_cell().store_int(v, vl))")
.on("uint", "uint")(() => "udict_replace_builder?(d, kl, k, begin_cell().store_uint(v, vl))")
.on("uint", "coins")(() => "udict_replace_builder?(d, kl, k, begin_cell().store_coins(v))")
.on("slice", "int")(() => "dict_replace_builder?(d, kl, k, begin_cell().store_int(v, vl))")
.on("slice", "uint")(() => "dict_replace_builder?(d, kl, k, begin_cell().store_uint(v, vl))")
.on("slice", "coins")(() => "dict_replace_builder?(d, kl, k, begin_cell().store_coins(v))")
.on("int", "cell")(() => "idict_replace_ref?(d, kl, k, v)")
.on("uint", "cell")(() => "udict_replace_ref?(d, kl, k, v)")
.on("int", "slice")(() => "idict_replace?(d, kl, k, v)")
.on("uint", "slice")(() => "udict_replace?(d, kl, k, v)")
.on("slice", "cell")(() => `${ctx.used("__tact_dict_replace_ref")}(d, kl, k, v)`)
.on("slice", "slice")(() => "dict_replace_builder?(d, kl, k, begin_cell().store_slice(v))")
.end();
ctx.fun(`__tact_dict_replace_${key}_${value}`, () => {
ctx.signature(
`(cell, (int)) __tact_dict_replace_${key}_${value}(cell d, int kl, ${signatureKeyType} k, ${signatureValueType} v${valBitsArg()})`,
Expand Down Expand Up @@ -1596,44 +1545,24 @@ function genTactDictReplaceGet(
return `idict_delete_get${cellSuffix}?`;
}
};
const returnExpr = () => {
switch (`${key}:${value}`) {
case "int:int":
return "d~idict_replaceget?(kl, k, begin_cell().store_int(v, vl).end_cell().begin_parse())";
case "int:uint":
return "d~idict_replaceget?(kl, k, begin_cell().store_uint(v, vl).end_cell().begin_parse())";
case "int:coins":
return "d~idict_replaceget?(kl, k, begin_cell().store_coins(v).end_cell().begin_parse())";
case "uint:int":
return "d~udict_replaceget?(kl, k, begin_cell().store_int(v, vl).end_cell().begin_parse())";
case "uint:uint":
return "d~udict_replaceget?(kl, k, begin_cell().store_uint(v, vl).end_cell().begin_parse())";
case "uint:coins":
return "d~udict_replaceget?(kl, k, begin_cell().store_coins(v).end_cell().begin_parse())";
case "slice:int":
return "d~dict_replaceget?(kl, k, begin_cell().store_int(v, vl).end_cell().begin_parse())";
case "slice:uint":
return "d~dict_replaceget?(kl, k, begin_cell().store_uint(v, vl).end_cell().begin_parse())";
case "slice:coins":
return "d~dict_replaceget?(kl, k, begin_cell().store_coins(v).end_cell().begin_parse())";
case "int:cell":
return "d~idict_replaceget_ref?(kl, k, v)";
case "uint:cell":
return "d~udict_replaceget_ref?(kl, k, v)";
case "int:slice":
return "d~idict_replaceget?(kl, k, v)";
case "uint:slice":
return "d~udict_replaceget?(kl, k, v)";
case "slice:cell":
return `d~${ctx.used("__tact_dict_replaceget_ref")}(kl, k, v)`;
case "slice:slice":
return "d~dict_replaceget?(kl, k, begin_cell().store_slice(v).end_cell().begin_parse())";
default:
throwInternalCompilerError(
`Unprocessed combination of key/value types ${key}/${value} in dict replaceGet operation`,
);
}
};
// prettier-ignore
const returnExpr = () => match(key, value)
.on("int", "int")(() => "d~idict_replaceget?(kl, k, begin_cell().store_int(v, vl).end_cell().begin_parse())")
.on("int", "uint")(() => "d~idict_replaceget?(kl, k, begin_cell().store_uint(v, vl).end_cell().begin_parse())")
.on("int", "coins")(() => "d~idict_replaceget?(kl, k, begin_cell().store_coins(v).end_cell().begin_parse())")
.on("uint", "int")(() => "d~udict_replaceget?(kl, k, begin_cell().store_int(v, vl).end_cell().begin_parse())")
.on("uint", "uint")(() => "d~udict_replaceget?(kl, k, begin_cell().store_uint(v, vl).end_cell().begin_parse())")
.on("uint", "coins")(() => "d~udict_replaceget?(kl, k, begin_cell().store_coins(v).end_cell().begin_parse())")
.on("slice", "int")(() => "d~dict_replaceget?(kl, k, begin_cell().store_int(v, vl).end_cell().begin_parse())")
.on("slice", "uint")(() => "d~dict_replaceget?(kl, k, begin_cell().store_uint(v, vl).end_cell().begin_parse())")
.on("slice", "coins")(() => "d~dict_replaceget?(kl, k, begin_cell().store_coins(v).end_cell().begin_parse())")
.on("int", "cell")(() => "d~idict_replaceget_ref?(kl, k, v)")
.on("uint", "cell")(() => "d~udict_replaceget_ref?(kl, k, v)")
.on("int", "slice")(() => "d~idict_replaceget?(kl, k, v)")
.on("uint", "slice")(() => "d~udict_replaceget?(kl, k, v)")
.on("slice", "cell")(() => `d~${ctx.used("__tact_dict_replaceget_ref")}(kl, k, v)`)
.on("slice", "slice")(() => "d~dict_replaceget?(kl, k, begin_cell().store_slice(v).end_cell().begin_parse())")
.end();
const parseExpr = () => {
switch (value) {
case "slice":
Expand Down
90 changes: 90 additions & 0 deletions src/utils/tricks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
type Extend<T extends any[], H> = H extends infer A ? [...T, A] : never;
type Flat<TS extends any[], R extends any[] = []> = TS extends [
infer H,
...infer T,
]
? Flat<T, Extend<R, H>>
: R;

declare const NoSuchCase: unique symbol;
interface NoSuchCaseBug<L> extends Array<never> {
[NoSuchCase]: L;
}
type On<I extends any[], O> = {
on: <const DI extends any[]>(
...key: I extends Flat<DI> ? DI : NoSuchCaseBug<DI>
) => <const DO>(
handler: (...args: Extract<I, Flat<DI>>) => DO,
) => MV<Exclude<I, Flat<DI>>, O | DO>;
};

declare const CasesAreNotExhaustive: unique symbol;
interface NonExhaustiveBug<L> {
[CasesAreNotExhaustive]: L;
}
type End<I extends any[], O> = [I] extends [never]
? EndInternal<I, O>
: {
otherwise: <const DO>(handle: (...input: I) => DO) => O | DO;
end: NonExhaustiveBug<I>;
};
type MV<I extends any[], O> = End<I, O> & On<I, O>;

type OnInternal<I extends any[], O> = {
on: <const DI extends any[]>(
...key: DI
) => <const DO>(
handler: (...args: Extract<I, Flat<DI>>) => DO,
) => MVInternal<Exclude<I, Flat<DI>>, O | DO>;
};
type EndInternal<I extends any[], O> = {
otherwise: <const DO>(handle: (...input: I) => DO) => O | DO;
end: () => O;
};
type MVInternal<I extends any[], O> = EndInternal<I, O> & OnInternal<I, O>;

const deepMatch = (a: unknown, b: unknown): boolean => {
if (
a === b &&
["number", "string", "boolean", "bigint"].includes(typeof a) &&
typeof a === typeof b
) {
return true;
}
if (a === null || b === null) {
return a === b;
}
if (typeof a === "object" && typeof b === "object") {
if (Array.isArray(a) && Array.isArray(b) && a.length === b.length) {
return a.every((a, i) => deepMatch(a, b[i]));
} else {
return Object.entries(b).every(([k, b]) =>
deepMatch(k in a ? (a as any)[k] : undefined, b),
);
}
}
return false;
};

export const match = <const I extends any[]>(
...args: I
): MV<Flat<I>, never> => {
const rec = <I extends any[], O>(end: () => O): MVInternal<I, O> => ({
end,
otherwise: (handler) => handler(...(args as unknown as I)),
on:
<const DI extends any[]>(...match: DI) =>
<const DO>(handler: (...args: Extract<I, Flat<DI>>) => DO) =>
rec<Exclude<I, Flat<DI>>, O | DO>(() =>
deepMatch(args, match)
? handler(
...(args as unknown as Extract<I, Flat<DI, []>>),
)
: end(),
),
});
return rec<Flat<I>, never>(() => {
throw new Error("Not exhaustive");
}) as MV<Flat<I>, never>;
};

0 comments on commit bb25297

Please sign in to comment.