diff --git a/.trunk/trunk.yaml b/.trunk/trunk.yaml index 391fb8c4..b93c2fe9 100644 --- a/.trunk/trunk.yaml +++ b/.trunk/trunk.yaml @@ -45,6 +45,5 @@ actions: disabled: - trunk-announce - trunk-check-pre-push - - trunk-fmt-pre-commit enabled: - trunk-upgrade-available diff --git a/README.md b/README.md index f40f0dd4..4eec799b 100644 --- a/README.md +++ b/README.md @@ -165,14 +165,20 @@ You can still add it as a dependency with a local copy: | [Keccak](https://github.com/kkrt-labs/cairo-vm-ts/issues/69) | ☑ | | [Poseidon](https://github.com/kkrt-labs/cairo-vm-ts/issues/71) | ☑ | | [Range Check 96](https://github.com/kkrt-labs/cairo-vm-ts/issues/81) | ☑ | -| Segment Arena | ☐ | +| [Segment Arena](https://github.com/kkrt-labs/cairo-vm-ts/pull/106) | ☑ | | AddMod | ☐ | | MulMod | ☐ | ### Hints - - +Hints are currently being implemented. + +Their development can be tracked +[here](https://github.com/kkrt-labs/cairo-vm-ts/issues/90). + +#### How to implement a hint ? + +A how-to guide has been written [here](/docs/howToImplementAHint.md). ### Differential Testing & Benchmark diff --git a/cairo_programs/cairo/hints/alloc_felt_dict.cairo b/cairo_programs/cairo/hints/alloc_felt_dict.cairo new file mode 100644 index 00000000..c7436f8a --- /dev/null +++ b/cairo_programs/cairo/hints/alloc_felt_dict.cairo @@ -0,0 +1,3 @@ +fn main() { + let mut _balances: Felt252Dict = Default::default(); +} diff --git a/cairo_programs/cairo/hints/array_append.cairo b/cairo_programs/cairo/hints/array_append.cairo new file mode 100644 index 00000000..5fe852c8 --- /dev/null +++ b/cairo_programs/cairo/hints/array_append.cairo @@ -0,0 +1,6 @@ +fn main() { + let mut a = ArrayTrait::new(); + a.append(0); + a.append(1); + a.append(2); +} diff --git a/cairo_programs/cairo/hints/dict_get.cairo b/cairo_programs/cairo/hints/dict_get.cairo new file mode 100644 index 00000000..5e8ab0b7 --- /dev/null +++ b/cairo_programs/cairo/hints/dict_get.cairo @@ -0,0 +1,5 @@ +fn main() { + let mut balances: Felt252Dict = Default::default(); + balances.insert('Simon', 100); + balances.get('Simon'); +} diff --git a/cairo_programs/cairo/hints/dict_insert.cairo b/cairo_programs/cairo/hints/dict_insert.cairo new file mode 100644 index 00000000..1a90b059 --- /dev/null +++ b/cairo_programs/cairo/hints/dict_insert.cairo @@ -0,0 +1,6 @@ +fn main() { + let mut balances: Felt252Dict = Default::default(); + balances.insert('Simon', 100); + balances.insert('Alice', 500); + balances.insert('Bob', 30); +} diff --git a/cairo_programs/cairo/hints/dict_multiple_insert.cairo b/cairo_programs/cairo/hints/dict_multiple_insert.cairo new file mode 100644 index 00000000..44bf6628 --- /dev/null +++ b/cairo_programs/cairo/hints/dict_multiple_insert.cairo @@ -0,0 +1,7 @@ +fn main() { + let mut balances: Felt252Dict = Default::default(); + balances.insert('Simon', 100); + balances.insert('Simon', 500); + balances.insert('Simon', 600); + balances.insert('Simon', 700); +} diff --git a/cairo_programs/cairo/hints/dict_squash.cairo b/cairo_programs/cairo/hints/dict_squash.cairo new file mode 100644 index 00000000..6778ac17 --- /dev/null +++ b/cairo_programs/cairo/hints/dict_squash.cairo @@ -0,0 +1,7 @@ +fn main() { + let mut dict = felt252_dict_new::(); + dict.insert(1, 64); + dict.insert(2, 75); + dict.insert(3, 75); + dict.squash(); +} diff --git a/cairo_programs/cairo/hints/multiple_dict.cairo b/cairo_programs/cairo/hints/multiple_dict.cairo new file mode 100644 index 00000000..8966c0b3 --- /dev/null +++ b/cairo_programs/cairo/hints/multiple_dict.cairo @@ -0,0 +1,6 @@ +fn main() { + let mut balances_1: Felt252Dict = Default::default(); + let mut balances_2: Felt252Dict = Default::default(); + balances_1.insert('Alice', 500); + balances_2.insert('Bob', 30); +} diff --git a/docs/howToImplementAHint.md b/docs/howToImplementAHint.md new file mode 100644 index 00000000..4573c866 --- /dev/null +++ b/docs/howToImplementAHint.md @@ -0,0 +1,155 @@ +# How to implement a hint + +Here is a how-to, using the hint `GetSegmentArenaIndex` as an example: + +## Find the signature + +Find the signature of the hint in the +[Cairo compiler](https://github.com/starkware-libs/cairo/blob/b741c26c553fd9fa3246cee91fd5c637f225cdb9/crates/cairo-lang-casm/src/hints/mod.rs): +[GetSegmentArenaIndex](https://github.com/starkware-libs/cairo/blob/b741c26c553fd9fa3246cee91fd5c637f225cdb9/crates/cairo-lang-casm/src/hints/mod.rs#L203) + +```rust +/// Retrieves the index of the given dict in the dict_infos segment. +GetSegmentArenaIndex { dict_end_ptr: ResOperand, dict_index: CellRef }, +``` + +Here, `dict_end_ptr` is a `ResOperand` while `dict_index` is a `CellRef`. + +The definitions of `Cellref` and `ResOperand` can be found in +`hintParamSchema.ts`. Hint arguments can only be one of these two types. + +## Parsing + +The Cairo VM takes the serialized compilation artifacts as input, we use Zod to +parse them. Each hint has its own parser object. + +The GetSegmentArenaIndex hint can be found in a format similar to this one: + +```json +"GetSegmentArenaIndex": { + "dict_end_ptr": { + "Deref": { + "register": "FP", + "offset": -3 + } + }, + "dict_index": { + "register": "FP", + "offset": 0 + } +} +``` + +- Add `GetSegmentArenaIndex` to the `HintName` enum in `src/hints/hintName.ts`. + It is used to identify the hint before executing it in a run. Hints are + ordered in an ascending alphabetical order. + + ```typescript + // hintName.ts + export enum HintName { + // ... + GetSegmentArenaIndex = 'GetSegmentArenaIndex', + } + ``` + +- Create the file `src/hints/dict/getSegmentArenaIndex.ts`. Place the file in + the appropriate sub-folder category, here `dict` because the hint is dedicated + to dictionaries. + +- Create and export a Zod object `getSegmentArenaIndexParser` which follows the + hint signature: + + ```typescript + // getSegmentArenaIndex.ts + export const getSegmentArenaIndexParser = z + .object({ + GetSegmentArenaIndex: z.object({ + dict_end_ptr: resOperand, + dict_index: cellRef, + }), + }) + .transform(({ GetSegmentArenaIndex: { dict_end_ptr, dict_index } }) => ({ + type: HintName.GetSegmentArenaIndex, + dictEndPtr: dict_end_ptr, + dictIndex: dict_index, + })); + ``` + + The parsed object must be transformed in two ways: + + 1. Enforce camelCase in fields name + 2. Add a field `type` which takes the corresponding value of the `HintName` + enum. + +- Add the parser to the Zod union `hint` in `src/hints/hintSchema.ts`: + + ```typescript + // hintSchema.ts + const hint = z.union([ + // ... + getSegmentArenaIndexParser, + ]); + ``` + +Now, we can implement the core logic of the hint. + +## Core Logic + +The core logic of the hint will be implemented in the same file as the hint +parser, here `getSegmentArenaIndex.ts`. The function implementing this logic +must be named as the camelCase version of the hint: `getSegmentArenaIndex()` +(similar to its filename). + +The parameters of the function are the virtual machine, as the hint must +interact with it, and the signature of the hint. + +So, in our case, the function signature would be +`export getSegmentArenaIndex(vm: VirtualMachine, dictEndPtr: ResOperand, dictIndex: CellRef)` + +To implement the logic, refer yourself to its implementation in the +[`cairo-vm`](https://github.com/lambdaclass/cairo-vm/blob/24c2349cc19832fd8c1552304fe0439765ed82c6/vm/src/hint_processor/cairo_1_hint_processor/hint_processor.rs#L427-L444) +and the +[`cairo-lang-runner`](https://github.com/starkware-libs/cairo/blob/b741c26c553fd9fa3246cee91fd5c637f225cdb9/crates/cairo-lang-runner/src/casm_run/mod.rs#L1873-L1880) +from the Cairo compiler. + +The last step is adding the hint to the handler object. + +## Handler + +The handler is defined in `src/hints/hintHandler.ts` + +It is a dictionary which maps a `HintName` value to a function executing the +corresponding core logic function. + +```typescript +export const handlers: Record< + HintName, + (vm: VirtualMachine, hint: Hint) => void +> = { + [HintName.GetSegmentArenaIndex]: (vm, hint) => { + const h = hint as GetSegmentArenaIndex; + getSegmentArenaIndex(vm, h.dictEndptr, h.dictIndex); + }, +}; +``` + +- Set the key as the HintName value, `HintName.GetSegmentArenaIndex` +- Set the value to a function which takes `(vm, hint)` as parameters and execute + the core logic function of the corresponding hint. + +To do so, we make a type assertion of the hint, matching the `HintName` value, +and we call the corresponding core logic function with the appropriate +arguments. + +The hint has been implemented, the last thing to do is testing it. + +## Testing + +Unit tests must test the correct parsing of the hint and the execution of the +core logic. Those tests are done in a `.test.ts` file in the same folder as the +hint. In our example, it would be `src/hints/dict/getSegmentArenaIndex.test.ts`. + +Integration test is done by creating a Cairo program in +`cairo_programs/cairo/hints`. We must verify its proper execution by compiling +it with `make compile` and executing it with the command +`cairo run path/to/my_program.json` diff --git a/src/builtins/builtin.ts b/src/builtins/builtin.ts index d9374643..36bf8dd7 100644 --- a/src/builtins/builtin.ts +++ b/src/builtins/builtin.ts @@ -7,6 +7,7 @@ import { poseidonHandler } from './poseidon'; import { keccakHandler } from './keccak'; import { outputHandler } from './output'; import { rangeCheckHandler } from './rangeCheck'; +import { segmentArenaHandler } from './segmentArena'; /** Proxy handler to abstract validation & deduction rules off the VM */ export type BuiltinHandler = ProxyHandler>; @@ -20,7 +21,7 @@ export type BuiltinHandler = ProxyHandler>; * - Keccak: Builtin for keccak hash family * - Pedersen: Builtin for pedersen hash family * - Poseidon: Builtin for poseidon hash family - * - Segment Arena: Unknown usage, must investigate + * - Segment Arena: Builtin to manage the dictionaries * - Output: Output builtin */ const BUILTIN_HANDLER: { @@ -35,6 +36,7 @@ const BUILTIN_HANDLER: { keccak: keccakHandler, range_check: rangeCheckHandler(128n), range_check96: rangeCheckHandler(96n), + segment_arena: segmentArenaHandler, }; /** Getter of the object `BUILTIN_HANDLER` */ diff --git a/src/builtins/segmentArena.ts b/src/builtins/segmentArena.ts new file mode 100644 index 00000000..e67c06d7 --- /dev/null +++ b/src/builtins/segmentArena.ts @@ -0,0 +1,30 @@ +import { BuiltinHandler } from './builtin'; + +/** + * Offset to compute the address + * of the info segment pointer. + */ +export const INFO_PTR_OFFSET = 3; + +/** + * Offset to read the current number + * of allocated dictionaries. + */ +export const DICT_NUMBER_OFFSET = 2; + +/** + * The segment arena builtin manages Cairo dictionaries. + * + * It works by block of 3 cells: + * - The first cell contains the base address of the info pointer. + * - The second cell contains the current number of allocated dictionaries. + * - The third cell contains the current number of squashed dictionaries. + * + * The Info segment is tightly closed to the segment arena builtin. + * + * It also works by block of 3 cells: + * - The first cell is the base address of a dictionary + * - The second cell is the end address of a dictionary when squashed. + * - The third cell is the current number of squashed dictionaries (i.e. its squashing index). + */ +export const segmentArenaHandler: BuiltinHandler = {}; diff --git a/src/errors/builtins.ts b/src/errors/builtins.ts index 78dab357..407f017e 100644 --- a/src/errors/builtins.ts +++ b/src/errors/builtins.ts @@ -22,11 +22,11 @@ export class RangeCheckOutOfBounds extends BuiltinError { } } -/** ECDSA signature cannot be retrieved from dictionnary at `offset` */ +/** ECDSA signature cannot be retrieved from dictionary at `offset` */ export class UndefinedECDSASignature extends BuiltinError { constructor(offset: number) { super( - `ECDSA signature cannot be retrieved from dictionnary at offset ${offset}` + `ECDSA signature cannot be retrieved from dictionary at offset ${offset}` ); } } @@ -48,7 +48,7 @@ public key (negative): ${pubKeyNegHex} } } -/** The signature dictionnary is undefined */ +/** The signature dictionary is undefined */ export class UndefinedSignatureDict extends BuiltinError {} /** An offset of type number is expected */ diff --git a/src/errors/dictionary.ts b/src/errors/dictionary.ts new file mode 100644 index 00000000..ad83c3e2 --- /dev/null +++ b/src/errors/dictionary.ts @@ -0,0 +1,10 @@ +import { Relocatable } from 'primitives/relocatable'; + +class DictionaryError extends Error {} + +/** Cannot find Dictionary at `address` */ +export class DictNotFound extends DictionaryError { + constructor(address: Relocatable) { + super(`Cannot found any Dictionary at address ${address.toString()}`); + } +} diff --git a/src/errors/hints.ts b/src/errors/hints.ts index d718a38a..050b1b27 100644 --- a/src/errors/hints.ts +++ b/src/errors/hints.ts @@ -3,20 +3,32 @@ import { Hint } from 'hints/hintSchema'; class HintError extends Error {} +/** The provided register at `cell` is incorrect. It must either be AP or FP. */ export class InvalidCellRefRegister extends HintError { constructor(cell: CellRef) { super(`Invalid register, expected AP or FP, got ${cell}`); } } +/** The operator of the BinOp operation `op` must either be `Add` or `Mul`. */ export class InvalidOperation extends HintError { constructor(op: string) { super(`Invalid BinOp operator - Expected Add or Mul, received ${op}`); } } +/** The hint to be executed is unknown. */ export class UnknownHint extends HintError { constructor(hint: Hint) { super(`Unknown hint: ${hint}`); } } + +/** The number of dict accesses is invalid. */ +export class InvalidDictAccessesNumber extends HintError { + constructor(ptrDiffValue: number, dictAccessSize: number) { + super( + `The number of dictionary accesses (${ptrDiffValue}) must be a multiple of the dictionary access size (${dictAccessSize})` + ); + } +} diff --git a/src/errors/memory.ts b/src/errors/memory.ts index 67ce35d0..752d152e 100644 --- a/src/errors/memory.ts +++ b/src/errors/memory.ts @@ -24,3 +24,10 @@ export class SegmentOutOfBounds extends MemoryError { ); } } + +/** Expected a SegmentValue but read `undefined` */ +export class UndefinedSegmentValue extends MemoryError { + constructor() { + super(`Expected a SegmentValue, read undefined`); + } +} diff --git a/src/errors/scopeManager.ts b/src/errors/scopeManager.ts new file mode 100644 index 00000000..36f703d4 --- /dev/null +++ b/src/errors/scopeManager.ts @@ -0,0 +1,15 @@ +class ScopeManagerError extends Error {} + +/** The main scope cannot be removed, there must always be at least one scope. */ +export class CannotExitMainScope extends ScopeManagerError { + constructor() { + super('Cannot exit the main scope'); + } +} + +/** The variable `name` is not accessible in the current scope. */ +export class VariableNotInScope extends ScopeManagerError { + constructor(name: string) { + super(`Variable ${name} is not in scope`); + } +} diff --git a/src/errors/squashedDictManager.ts b/src/errors/squashedDictManager.ts new file mode 100644 index 00000000..0e31acf1 --- /dev/null +++ b/src/errors/squashedDictManager.ts @@ -0,0 +1,28 @@ +import { Felt } from 'primitives/felt'; + +class SquashedDictManagerError extends Error {} + +/** There are no keys left in the squashed dictionary manager */ +export class EmptyKeys extends SquashedDictManagerError { + constructor() { + super('There are no keys left in the squashed dictionary manager.'); + } +} + +/** There are no indices at `key` */ +export class EmptyIndices extends SquashedDictManagerError { + constructor(key: Felt | undefined) { + super( + `There are no indices at key ${ + key ? key.toString() : key + } in the squashed dictionary manager.` + ); + } +} + +/** The last index of the squashed dictionary manager is empty. */ +export class EmptyIndex extends SquashedDictManagerError { + constructor() { + super('The last index of the squashed dictionary manager is empty.'); + } +} diff --git a/src/errors/virtualMachine.ts b/src/errors/virtualMachine.ts index f082aec0..4542882a 100644 --- a/src/errors/virtualMachine.ts +++ b/src/errors/virtualMachine.ts @@ -1,3 +1,4 @@ +import { ResOperand } from 'hints/hintParamsSchema'; import { Relocatable } from 'primitives/relocatable'; import { SegmentValue } from 'primitives/segmentValue'; @@ -57,3 +58,10 @@ export class UndefinedOp1 extends VirtualMachineError { super('op1 is undefined'); } } + +/** `resOperand` is not of a valid type to extract buffer from it. */ +export class InvalidBufferResOp extends VirtualMachineError { + constructor(resOperand: ResOperand) { + super(`Cannot extract buffer from the given ResOperand: ${resOperand}`); + } +} diff --git a/src/hints/allocSegment.test.ts b/src/hints/allocSegment.test.ts index 71ef19f0..730fdfa4 100644 --- a/src/hints/allocSegment.test.ts +++ b/src/hints/allocSegment.test.ts @@ -3,6 +3,7 @@ import { describe, expect, test } from 'bun:test'; import { Relocatable } from 'primitives/relocatable'; import { Register } from 'vm/instruction'; import { VirtualMachine } from 'vm/virtualMachine'; + import { HintName } from 'hints/hintName'; import { allocSegmentParser } from './allocSegment'; diff --git a/src/hints/allocSegment.ts b/src/hints/allocSegment.ts index d6be40c0..572df684 100644 --- a/src/hints/allocSegment.ts +++ b/src/hints/allocSegment.ts @@ -1,8 +1,9 @@ import { z } from 'zod'; import { VirtualMachine } from 'vm/virtualMachine'; -import { cellRef, CellRef } from 'hints/hintParamsSchema'; + import { HintName } from 'hints/hintName'; +import { cellRef, CellRef } from 'hints/hintParamsSchema'; /** Zod object to parse AllocSegment hint */ export const allocSegmentParser = z diff --git a/src/hints/assertLeFindSmallArc.ts b/src/hints/assertLeFindSmallArc.ts new file mode 100644 index 00000000..34987836 --- /dev/null +++ b/src/hints/assertLeFindSmallArc.ts @@ -0,0 +1,70 @@ +import { z } from 'zod'; + +import { Felt } from 'primitives/felt'; +import { VirtualMachine } from 'vm/virtualMachine'; + +import { HintName } from 'hints/hintName'; +import { ResOperand, resOperand } from 'hints/hintParamsSchema'; + +/** Zod object to parse AssertLeFindSmallArcs hint */ +export const assertLeFindSmallArcsParser = z + .object({ + AssertLeFindSmallArcs: z.object({ + range_check_ptr: resOperand, + a: resOperand, + b: resOperand, + }), + }) + .transform(({ AssertLeFindSmallArcs: { range_check_ptr, a, b } }) => ({ + type: HintName.AssertLeFindSmallArcs, + rangeCheckPtr: range_check_ptr, + a, + b, + })); + +/** + * AssertLeFindSmallArcs hint + * + * Find the two small arcs from [(0, a), (a, b), (b, PRIME)] and + * assert them to the range check segment. + */ +export type AssertLeFindSmallArcs = z.infer; + +export type Arc = { + value: Felt; + pos: number; +}; + +/** + * Compute the three arcs [(0, a), (a, b), (b, PRIME)] + * + * Set the biggest arc to the scope variable `excluded_arc` + * + * Store the modulo and remainder of the smallest arc (resp. second smallest arc) + * against `Math.ceil(Felt.PRIME / 3) >> 128n` (resp. `Math.ceil(Felt.PRIME / 2) >> 128n`) + */ +export const assertLeFindSmallArcs = ( + vm: VirtualMachine, + rangeCheckPtr: ResOperand, + a: ResOperand, + b: ResOperand +) => { + const aVal = vm.getResOperandValue(a); + const bVal = vm.getResOperandValue(b); + const arcs: Arc[] = [ + { value: aVal, pos: 0 }, + { value: bVal.sub(aVal), pos: 1 }, + { value: new Felt(-1n).sub(bVal), pos: 2 }, + ].sort((a, b) => a.value.compare(b.value)); + + vm.scopeManager.set('excluded_arc', arcs[2].pos); + + const primeOver3High = new Felt(3544607988759775765608368578435044694n); + const primeOver2High = new Felt(5316911983139663648412552867652567041n); + const ptr = vm.getPointer(...vm.extractBuffer(rangeCheckPtr)); + + vm.memory.assertEq(ptr, arcs[0].value.mod(primeOver3High)); + vm.memory.assertEq(ptr.add(1), arcs[0].value.div(primeOver3High)); + vm.memory.assertEq(ptr.add(2), arcs[1].value.mod(primeOver2High)); + vm.memory.assertEq(ptr.add(3), arcs[1].value.div(primeOver2High)); +}; diff --git a/src/hints/assertLeIsFirstArcExcluded.ts b/src/hints/assertLeIsFirstArcExcluded.ts new file mode 100644 index 00000000..5986765c --- /dev/null +++ b/src/hints/assertLeIsFirstArcExcluded.ts @@ -0,0 +1,43 @@ +import { z } from 'zod'; + +import { Felt } from 'primitives/felt'; +import { VirtualMachine } from 'vm/virtualMachine'; + +import { HintName } from 'hints/hintName'; +import { cellRef, CellRef } from 'hints/hintParamsSchema'; +import { Arc } from './assertLeFindSmallArc'; + +/** Zod object to parse AssertLeIsFirstArcExcluded hint */ +export const assertLeIsFirstArcExcludedParser = z + .object({ + AssertLeIsFirstArcExcluded: z.object({ skip_exclude_a_flag: cellRef }), + }) + .transform(({ AssertLeIsFirstArcExcluded: { skip_exclude_a_flag } }) => ({ + type: HintName.AssertLeIsFirstArcExcluded, + skipExcludeFirstArc: skip_exclude_a_flag, + })); + +/** + * AssertLeIsFirstArcExcluded hint + * + * Assert if the arc (0, a) was excluded. + */ +export type AssertLeIsFirstArcExcluded = z.infer< + typeof assertLeIsFirstArcExcludedParser +>; + +/** + * Assert if the arc (0, a) from `AssertLeFindSmallArcs` was excluded. + * + * Read the value in scope at `excluded_arc` + */ +export const assertLeIsFirstArcExcluded = ( + vm: VirtualMachine, + skipExcludeFirstArc: CellRef +) => { + const excludedArc = vm.scopeManager.get('excluded_arc'); + vm.memory.assertEq( + vm.cellRefToRelocatable(skipExcludeFirstArc), + (excludedArc as Arc).pos != 0 ? new Felt(1n) : new Felt(0n) + ); +}; diff --git a/src/hints/assertLeIsSecondArcExcluded.ts b/src/hints/assertLeIsSecondArcExcluded.ts new file mode 100644 index 00000000..c8df62a2 --- /dev/null +++ b/src/hints/assertLeIsSecondArcExcluded.ts @@ -0,0 +1,43 @@ +import { z } from 'zod'; + +import { Felt } from 'primitives/felt'; +import { VirtualMachine } from 'vm/virtualMachine'; + +import { HintName } from 'hints/hintName'; +import { cellRef, CellRef } from 'hints/hintParamsSchema'; +import { Arc } from './assertLeFindSmallArc'; + +/** Zod object to parse AssertLeIsSecondArcExcluded hint */ +export const assertLeIsSecondArcExcludedParser = z + .object({ + AssertLeIsSecondArcExcluded: z.object({ skip_exclude_b_minus_a: cellRef }), + }) + .transform(({ AssertLeIsSecondArcExcluded: { skip_exclude_b_minus_a } }) => ({ + type: HintName.AssertLeIsSecondArcExcluded, + skipExcludeSecondArc: skip_exclude_b_minus_a, + })); + +/** + * AssertLeIsSecondArcExcluded hint + * + * Assert if the arc (a, b) was excluded. + */ +export type AssertLeIsSecondArcExcluded = z.infer< + typeof assertLeIsSecondArcExcludedParser +>; + +/** + * Assert if the arc (a, b) from `AssertLeFindSmallArcs` was excluded. + * + * Read the value in scope at `excluded_arc` + */ +export const assertLeIsSecondArcExcluded = ( + vm: VirtualMachine, + skipExcludeSecondArc: CellRef +) => { + const excludedArc = vm.scopeManager.get('excluded_arc'); + vm.memory.assertEq( + vm.cellRefToRelocatable(skipExcludeSecondArc), + (excludedArc as Arc).pos != 1 ? new Felt(1n) : new Felt(0n) + ); +}; diff --git a/src/hints/dict/allocFelt252Dict.test.ts b/src/hints/dict/allocFelt252Dict.test.ts new file mode 100644 index 00000000..e435eac2 --- /dev/null +++ b/src/hints/dict/allocFelt252Dict.test.ts @@ -0,0 +1,70 @@ +import { describe, expect, test } from 'bun:test'; + +import { Felt } from 'primitives/felt'; +import { Relocatable } from 'primitives/relocatable'; +import { Register } from 'vm/instruction'; +import { VirtualMachine } from 'vm/virtualMachine'; +import { segmentArenaHandler } from 'builtins/segmentArena'; + +import { HintName } from 'hints/hintName'; +import { OpType } from 'hints/hintParamsSchema'; +import { allocFelt252DictParser } from './allocFelt252Dict'; + +const initSegmentArenaBuiltin = (vm: VirtualMachine) => { + const info = [ + vm.memory.addSegment(segmentArenaHandler), + new Felt(0n), + new Felt(0n), + ]; + const base = vm.memory.addSegment(segmentArenaHandler); + info.map((value, offset) => vm.memory.assertEq(base.add(offset), value)); + return base.add(info.length); +}; + +const ALLOC_FELT252_DICT = { + AllocFelt252Dict: { + segment_arena_ptr: { + Deref: { + register: 'AP', + offset: 0, + }, + }, + }, +}; + +describe('AllocFelt252Dict', () => { + test('should properly parse AllocFelt252Dict hint', () => { + const hint = allocFelt252DictParser.parse(ALLOC_FELT252_DICT); + expect(hint).toEqual({ + type: HintName.AllocFelt252Dict, + segmentArenaPtr: { + type: OpType.Deref, + cell: { + register: Register.Ap, + offset: 0, + }, + }, + }); + }); + + test('should properly execute AllocFelt252Dict hint', () => { + const hint = allocFelt252DictParser.parse(ALLOC_FELT252_DICT); + const vm = new VirtualMachine(); + vm.memory.addSegment(); + vm.memory.addSegment(); + const arenaPtr = initSegmentArenaBuiltin(vm); + + expect(vm.memory.get(arenaPtr.sub(1))).toEqual(new Felt(0n)); + expect(vm.memory.get(arenaPtr.sub(2))).toEqual(new Felt(0n)); + expect(vm.memory.get(arenaPtr.sub(3))).toEqual(new Relocatable(2, 0)); + expect(vm.dictManager.size).toEqual(0); + + vm.memory.assertEq(vm.ap, arenaPtr); + vm.executeHint(hint); + + const newDictPtr = new Relocatable(4, 0); + expect(vm.memory.get(new Relocatable(2, 0))).toEqual(newDictPtr); + expect(vm.dictManager.size).toEqual(1); + expect(vm.dictManager.has(newDictPtr.segmentId)).toBeTrue(); + }); +}); diff --git a/src/hints/dict/allocFelt252Dict.ts b/src/hints/dict/allocFelt252Dict.ts new file mode 100644 index 00000000..afebc058 --- /dev/null +++ b/src/hints/dict/allocFelt252Dict.ts @@ -0,0 +1,56 @@ +import { z } from 'zod'; + +import { ExpectedFelt, ExpectedRelocatable } from 'errors/primitives'; + +import { isFelt, isRelocatable } from 'primitives/segmentValue'; +import { VirtualMachine } from 'vm/virtualMachine'; + +import { HintName } from 'hints/hintName'; +import { resOperand, ResOperand } from 'hints/hintParamsSchema'; +import { DICT_ACCESS_SIZE } from 'hints/dictionary'; +import { DICT_NUMBER_OFFSET, INFO_PTR_OFFSET } from 'builtins/segmentArena'; + +/** Zod object to parse AllocFelt252Dict hint */ +export const allocFelt252DictParser = z + .object({ AllocFelt252Dict: z.object({ segment_arena_ptr: resOperand }) }) + .transform(({ AllocFelt252Dict: { segment_arena_ptr } }) => ({ + type: HintName.AllocFelt252Dict, + segmentArenaPtr: segment_arena_ptr, + })); + +/** + * AllocFelt252Dict hint + * + * Allocates a new dictionary using the segment arena builtin. + */ +export type AllocFelt252Dict = z.infer; + +/** + * Allocates a new dictionary using the segment arena builtin. + * + * The segment arena builtin works by block of 3 cells: + * - The first cell contains the base address of the info segment. + * - The second cell contains the current number of allocated dictionaries. + * - The third cell contains the current number of squashed dictionaries. + * + * @param {VirtualMachine} vm - The virtual machine instance. + * @param {ResOperand} segmentArenaPtr - Pointer to the first cell of the next segment arena block. + * @throws {ExpectedFelt} If the number of dictionaries is not a valid Felt. + * @throws {ExpectedRelocatable} If the info pointer read is not a valid Relocatable. + + */ +export const allocFelt252Dict = ( + vm: VirtualMachine, + segmentArenaPtr: ResOperand +) => { + const arenaPtr = vm.getPointer(...vm.extractBuffer(segmentArenaPtr)); + const dictNumber = vm.memory.get(arenaPtr.sub(DICT_NUMBER_OFFSET)); + if (!dictNumber || !isFelt(dictNumber)) throw new ExpectedFelt(dictNumber); + + const infoPtr = vm.memory.get(arenaPtr.sub(INFO_PTR_OFFSET)); + if (!infoPtr || !isRelocatable(infoPtr)) + throw new ExpectedRelocatable(infoPtr); + + const newDict = vm.newDict(); + vm.memory.assertEq(infoPtr.add(dictNumber.mul(DICT_ACCESS_SIZE)), newDict); +}; diff --git a/src/hints/dict/felt252DictEntryInit.test.ts b/src/hints/dict/felt252DictEntryInit.test.ts new file mode 100644 index 00000000..d920b1fe --- /dev/null +++ b/src/hints/dict/felt252DictEntryInit.test.ts @@ -0,0 +1,104 @@ +import { describe, expect, test } from 'bun:test'; + +import { Felt } from 'primitives/felt'; +import { Relocatable } from 'primitives/relocatable'; +import { Register } from 'vm/instruction'; +import { VirtualMachine } from 'vm/virtualMachine'; +import { segmentArenaHandler } from 'builtins/segmentArena'; + +import { HintName } from 'hints/hintName'; +import { OpType } from 'hints/hintParamsSchema'; +import { allocFelt252DictParser } from './allocFelt252Dict'; +import { felt252DictEntryInitParser } from './felt252DictEntryInit'; + +const initSegmentArenaBuiltin = (vm: VirtualMachine) => { + const info = [ + vm.memory.addSegment(segmentArenaHandler), + new Felt(0n), + new Felt(0n), + ]; + const base = vm.memory.addSegment(segmentArenaHandler); + info.map((value, offset) => vm.memory.assertEq(base.add(offset), value)); + return base.add(info.length); +}; + +const ALLOC_FELT252_DICT = { + AllocFelt252Dict: { + segment_arena_ptr: { + Deref: { + register: 'AP', + offset: 0, + }, + }, + }, +}; + +const FELT252_DICT_ENTRY_INIT = { + Felt252DictEntryInit: { + dict_ptr: { + Deref: { + register: 'AP', + offset: 1, + }, + }, + key: { + Deref: { + register: 'AP', + offset: 2, + }, + }, + }, +}; + +describe('Felt252DictEntryInit', () => { + test('should properly parse Felt252DictEntryInit hint', () => { + const hint = felt252DictEntryInitParser.parse(FELT252_DICT_ENTRY_INIT); + expect(hint).toEqual({ + type: HintName.Felt252DictEntryInit, + dictPtr: { + type: OpType.Deref, + cell: { + register: Register.Ap, + offset: 1, + }, + }, + key: { + type: OpType.Deref, + cell: { + register: Register.Ap, + offset: 2, + }, + }, + }); + }); + + test('should properly execute Felt252DictEntryInit hint', () => { + const allocHint = allocFelt252DictParser.parse(ALLOC_FELT252_DICT); + const hint = felt252DictEntryInitParser.parse(FELT252_DICT_ENTRY_INIT); + const vm = new VirtualMachine(); + vm.memory.addSegment(); + vm.memory.addSegment(); + const arenaPtr = initSegmentArenaBuiltin(vm); + + vm.memory.assertEq(vm.ap, arenaPtr); + vm.executeHint(allocHint); + + const newDictPtr = new Relocatable(4, 0); + const key = new Felt(5n); + vm.memory.assertEq(vm.ap.add(1), newDictPtr); + vm.memory.assertEq(vm.ap.add(2), key); + + const dict = vm.dictManager.get(newDictPtr.segmentId); + expect(dict).not.toBeUndefined(); + + const keyValue = new Felt(13n); + dict?.set(key.toString(), keyValue); + + vm.executeHint(hint); + + if (dict) { + expect(dict.get(key.toString())).toEqual(new Felt(13n)); + expect(vm.memory.get(newDictPtr.add(1))).toEqual(keyValue); + } + }); +}); diff --git a/src/hints/dict/felt252DictEntryInit.ts b/src/hints/dict/felt252DictEntryInit.ts new file mode 100644 index 00000000..9c97f3ee --- /dev/null +++ b/src/hints/dict/felt252DictEntryInit.ts @@ -0,0 +1,57 @@ +import { z } from 'zod'; + +import { Felt } from 'primitives/felt'; +import { VirtualMachine } from 'vm/virtualMachine'; + +import { resOperand, ResOperand } from 'hints/hintParamsSchema'; +import { HintName } from 'hints/hintName'; +import { PREV_VALUE_OFFSET } from 'hints/dictionary'; + +/** Zod object to parse Felt252DictEntryInit hint */ +export const felt252DictEntryInitParser = z + .object({ + Felt252DictEntryInit: z.object({ + dict_ptr: resOperand, + key: resOperand, + }), + }) + .transform(({ Felt252DictEntryInit: { dict_ptr, key } }) => ({ + type: HintName.Felt252DictEntryInit, + dictPtr: dict_ptr, + key, + })); + +/** + * Felt252DictEntryInit hint + * + * Initializes a dictionary access by asserting that + * the previous value cell holds the current value + * of the dictionary (initialized to zero if undefined). + * + */ +export type Felt252DictEntryInit = z.infer; + +/** + * Initializes a dictionary access to a key by asserting that + * the previous value cell contains the current value + * of this dictionary key. + * + * If it is the first entry for this key, + * the previous value will be zero. + * + * @param {VirtualMachine} vm - The virtual machine instance. + * @param {ResOperand} dictPtr - Pointer to the dictionary access to be initialized. + * @param key - The dictionary key to access to. + */ +export const felt252DictEntryInit = ( + vm: VirtualMachine, + dictPtr: ResOperand, + key: ResOperand +) => { + const address = vm.getPointer(...vm.extractBuffer(dictPtr)); + const keyValue = vm.getResOperandValue(key).toString(); + const dict = vm.getDict(address); + const prevValue = dict.get(keyValue) || new Felt(0n); + dict.set(keyValue, prevValue); + vm.memory.assertEq(address.add(PREV_VALUE_OFFSET), prevValue); +}; diff --git a/src/hints/dict/felt252DictEntryUpdate.test.ts b/src/hints/dict/felt252DictEntryUpdate.test.ts new file mode 100644 index 00000000..29fd5cbc --- /dev/null +++ b/src/hints/dict/felt252DictEntryUpdate.test.ts @@ -0,0 +1,107 @@ +import { describe, expect, test } from 'bun:test'; + +import { Felt } from 'primitives/felt'; +import { Relocatable } from 'primitives/relocatable'; +import { SegmentValue } from 'primitives/segmentValue'; +import { Register } from 'vm/instruction'; +import { VirtualMachine } from 'vm/virtualMachine'; +import { segmentArenaHandler } from 'builtins/segmentArena'; + +import { HintName } from 'hints/hintName'; +import { OpType } from 'hints/hintParamsSchema'; +import { allocFelt252DictParser } from './allocFelt252Dict'; +import { felt252DictEntryUpdateParser } from './felt252DictEntryUpdate'; + +const initSegmentArenaBuiltin = (vm: VirtualMachine) => { + const info = [ + vm.memory.addSegment(segmentArenaHandler), + new Felt(0n), + new Felt(0n), + ]; + const base = vm.memory.addSegment(segmentArenaHandler); + info.map((value, offset) => vm.memory.assertEq(base.add(offset), value)); + return base.add(info.length); +}; + +const ALLOC_FELT252_DICT = { + AllocFelt252Dict: { + segment_arena_ptr: { + Deref: { + register: 'AP', + offset: 0, + }, + }, + }, +}; + +const FELT252_DICT_ENTRY_UPDATE = { + Felt252DictEntryUpdate: { + dict_ptr: { + Deref: { + register: 'AP', + offset: 1, + }, + }, + value: { + Deref: { + register: 'AP', + offset: 3, + }, + }, + }, +}; + +describe('Felt252DictEntryUpdate', () => { + test('should properly parse Felt252DictEntryUpdate hint', () => { + const hint = felt252DictEntryUpdateParser.parse(FELT252_DICT_ENTRY_UPDATE); + expect(hint).toEqual({ + type: HintName.Felt252DictEntryUpdate, + dictPtr: { + type: OpType.Deref, + cell: { + register: Register.Ap, + offset: 1, + }, + }, + value: { + type: OpType.Deref, + cell: { + register: Register.Ap, + offset: 3, + }, + }, + }); + }); + + test.each([new Felt(13n), new Relocatable(2, 0)])( + 'should properly execute Felt252DictUpdate hint', + (value: SegmentValue) => { + const allocHint = allocFelt252DictParser.parse(ALLOC_FELT252_DICT); + const hint = felt252DictEntryUpdateParser.parse( + FELT252_DICT_ENTRY_UPDATE + ); + const vm = new VirtualMachine(); + vm.memory.addSegment(); + vm.memory.addSegment(); + const arenaPtr = initSegmentArenaBuiltin(vm); + + vm.memory.assertEq(vm.ap, arenaPtr); + vm.executeHint(allocHint); + + const newDictPtr = new Relocatable(4, 0); + const keyValue = new Felt(5n); + vm.memory.assertEq(vm.ap.add(1), newDictPtr.add(3)); + vm.memory.assertEq(newDictPtr, keyValue); + vm.memory.assertEq(vm.ap.add(3), value); + + const dict = vm.dictManager.get(newDictPtr.segmentId); + expect(dict).not.toBeUndefined(); + + vm.executeHint(hint); + + if (dict) { + expect(dict.get(keyValue.toString())).toEqual(value); + } + } + ); +}); diff --git a/src/hints/dict/felt252DictEntryUpdate.ts b/src/hints/dict/felt252DictEntryUpdate.ts new file mode 100644 index 00000000..01e2bd22 --- /dev/null +++ b/src/hints/dict/felt252DictEntryUpdate.ts @@ -0,0 +1,55 @@ +import { z } from 'zod'; + +import { ExpectedFelt } from 'errors/primitives'; + +import { isFelt } from 'primitives/segmentValue'; +import { VirtualMachine } from 'vm/virtualMachine'; + +import { HintName } from 'hints/hintName'; +import { Deref, OpType, resOperand, ResOperand } from 'hints/hintParamsSchema'; +import { KEY_OFFSET } from 'hints/dictionary'; + +/** Zod object to parse Felt252DictEntryUpdate hint */ +export const felt252DictEntryUpdateParser = z + .object({ + Felt252DictEntryUpdate: z.object({ + dict_ptr: resOperand, + value: resOperand, + }), + }) + .transform(({ Felt252DictEntryUpdate: { dict_ptr, value } }) => ({ + type: HintName.Felt252DictEntryUpdate, + dictPtr: dict_ptr, + value, + })); + +/** + * Felt252DictEntryUpdate hint + * + * Updates a dictionary entry. + */ +export type Felt252DictEntryUpdate = z.infer< + typeof felt252DictEntryUpdateParser +>; + +/** + * Updates a dictionary entry. + * + * @param {VirtualMachine} vm - The virtual machine instance. + * @param {ResOperand} dictPtr - Pointer to the next dictionary access. + * @param {ResOperand} value - The new value to be set. + */ +export const felt252DictEntryUpdate = ( + vm: VirtualMachine, + dictPtr: ResOperand, + value: ResOperand +) => { + const address = vm.getPointer(...vm.extractBuffer(dictPtr)); + const key = vm.memory.get(address.sub(KEY_OFFSET)); + if (!key || !isFelt(key)) throw new ExpectedFelt(key); + const val = + value.type === OpType.Deref + ? vm.getSegmentValue((value as Deref).cell) + : vm.getResOperandValue(value); + vm.getDict(address).set(key.toString(), val); +}; diff --git a/src/hints/dict/getCurrentAccessDelta.test.ts b/src/hints/dict/getCurrentAccessDelta.test.ts new file mode 100644 index 00000000..82c5a1d4 --- /dev/null +++ b/src/hints/dict/getCurrentAccessDelta.test.ts @@ -0,0 +1,48 @@ +import { describe, expect, test } from 'bun:test'; + +import { Felt } from 'primitives/felt'; +import { Register } from 'vm/instruction'; +import { VirtualMachine } from 'vm/virtualMachine'; + +import { HintName } from 'hints/hintName'; +import { getCurrentAccessDeltaParser } from './getCurrentAccessDelta'; + +const GET_CURRENT_ACCESS_INDEX = { + GetCurrentAccessDelta: { + index_delta_minus1: { + register: 'AP', + offset: 0, + }, + }, +}; + +describe('GetCurrentAccessDelta', () => { + test('should properly parse GetCurrentAccessDelta hint', () => { + const hint = getCurrentAccessDeltaParser.parse(GET_CURRENT_ACCESS_INDEX); + expect(hint).toEqual({ + type: HintName.GetCurrentAccessDelta, + indexDeltaMinusOne: { + register: Register.Ap, + offset: 0, + }, + }); + }); + + test('should properly execute GetCurrentAccessDelta hint', () => { + const hint = getCurrentAccessDeltaParser.parse(GET_CURRENT_ACCESS_INDEX); + const vm = new VirtualMachine(); + vm.memory.addSegment(); + vm.memory.addSegment(); + + const key = new Felt(0n); + const poppedIndex = new Felt(3n); + const lastIndex = new Felt(35n); + const indices = [new Felt(40n), lastIndex, poppedIndex]; + vm.squashedDictManager.keyToIndices.set(key.toString(), indices); + vm.squashedDictManager.keys = [key]; + + vm.executeHint(hint); + + expect(vm.memory.get(vm.ap)).toEqual(lastIndex.sub(poppedIndex).sub(1)); + }); +}); diff --git a/src/hints/dict/getCurrentAccessDelta.ts b/src/hints/dict/getCurrentAccessDelta.ts new file mode 100644 index 00000000..bb83c0f0 --- /dev/null +++ b/src/hints/dict/getCurrentAccessDelta.ts @@ -0,0 +1,41 @@ +import { z } from 'zod'; + +import { VirtualMachine } from 'vm/virtualMachine'; + +import { HintName } from 'hints/hintName'; +import { cellRef, CellRef } from 'hints/hintParamsSchema'; + +/** Zod object to parse GetCurrentAccessDelta hint */ +export const getCurrentAccessDeltaParser = z + .object({ GetCurrentAccessDelta: z.object({ index_delta_minus1: cellRef }) }) + .transform(({ GetCurrentAccessDelta: { index_delta_minus1 } }) => ({ + type: HintName.GetCurrentAccessDelta, + indexDeltaMinusOne: index_delta_minus1, + })); + +/** + * GetCurrentAccessDelta hint + * + * Computes the delta between the last two indices. + * + */ +export type GetCurrentAccessDelta = z.infer; + +/** + * Computes the delta between the last two indices. + * + * @param {VirtualMachine} vm - The virtual machine instance + * @param {CellRef} indexDeltaMinusOne - The address where the index delta should be asserted. + */ +export const getCurrentAccessDelta = ( + vm: VirtualMachine, + indexDeltaMinusOne: CellRef +) => { + const prevIndex = vm.squashedDictManager.popIndex(); + const currIndex = vm.squashedDictManager.lastIndex(); + + vm.memory.assertEq( + vm.cellRefToRelocatable(indexDeltaMinusOne), + currIndex.sub(prevIndex).sub(1) + ); +}; diff --git a/src/hints/dict/getCurrentAccessIndex.test.ts b/src/hints/dict/getCurrentAccessIndex.test.ts new file mode 100644 index 00000000..5912db53 --- /dev/null +++ b/src/hints/dict/getCurrentAccessIndex.test.ts @@ -0,0 +1,56 @@ +import { describe, expect, test } from 'bun:test'; + +import { Felt } from 'primitives/felt'; +import { Register } from 'vm/instruction'; +import { VirtualMachine } from 'vm/virtualMachine'; +import { rangeCheckHandler } from 'builtins/rangeCheck'; + +import { HintName } from 'hints/hintName'; +import { OpType } from 'hints/hintParamsSchema'; +import { getCurrentAccessIndexParser } from './getCurrentAccessIndex'; + +const GET_CURRENT_ACCESS_INDEX = { + GetCurrentAccessIndex: { + range_check_ptr: { + Deref: { + register: 'AP', + offset: 0, + }, + }, + }, +}; + +describe('GetCurrentAccessIndex', () => { + test('should properly parse GetCurrentAccessIndex hint', () => { + const hint = getCurrentAccessIndexParser.parse(GET_CURRENT_ACCESS_INDEX); + expect(hint).toEqual({ + type: HintName.GetCurrentAccessIndex, + rangeCheckPtr: { + type: OpType.Deref, + cell: { + register: Register.Ap, + offset: 0, + }, + }, + }); + }); + + test('should properly execute GetCurrentAccessIndex hint', () => { + const hint = getCurrentAccessIndexParser.parse(GET_CURRENT_ACCESS_INDEX); + const vm = new VirtualMachine(); + vm.memory.addSegment(); + vm.memory.addSegment(); + const rangeCheck = vm.memory.addSegment(rangeCheckHandler(128n)); + + vm.memory.assertEq(vm.ap, rangeCheck); + + const key = new Felt(0n); + const indices = [new Felt(35n), new Felt(6n), new Felt(3n)]; + vm.squashedDictManager.keyToIndices.set(key.toString(), indices); + vm.squashedDictManager.keys = [key]; + + vm.executeHint(hint); + + expect(vm.memory.get(rangeCheck)).toEqual(indices[indices.length - 1]); + }); +}); diff --git a/src/hints/dict/getCurrentAccessIndex.ts b/src/hints/dict/getCurrentAccessIndex.ts new file mode 100644 index 00000000..d853787c --- /dev/null +++ b/src/hints/dict/getCurrentAccessIndex.ts @@ -0,0 +1,40 @@ +import { z } from 'zod'; + +import { VirtualMachine } from 'vm/virtualMachine'; + +import { resOperand, ResOperand } from 'hints/hintParamsSchema'; +import { HintName } from 'hints/hintName'; + +/** Zod object to parse GetCurrentAccessIndex hint */ +export const getCurrentAccessIndexParser = z + .object({ GetCurrentAccessIndex: z.object({ range_check_ptr: resOperand }) }) + .transform(({ GetCurrentAccessIndex: { range_check_ptr } }) => ({ + type: HintName.GetCurrentAccessIndex, + rangeCheckPtr: range_check_ptr, + })); + +/** + * GetCurrentAccessIndex hint + * Assert that the accessed index is strictly inferior to 2 ** 128. + * + * A dictionary cannot have more than 2 ** 128 accesses in a single run. + */ +export type GetCurrentAccessIndex = z.infer; + +/** + * Assert that the accessed index is strictly inferior to 2 ** 128. + * + * A dictionary cannot have more than 2 ** 128 accesses in a single run. + * + * @param {VirtualMachine} vm - The virtual machine instance + * @param {ResOperand} rangeCheckPtr - Pointer to the range check builtin. + */ +export const getCurrentAccessIndex = ( + vm: VirtualMachine, + rangeCheckPtr: ResOperand +) => { + vm.memory.assertEq( + vm.getPointer(...vm.extractBuffer(rangeCheckPtr)), + vm.squashedDictManager.lastIndex() + ); +}; diff --git a/src/hints/dict/getNextDictKey.test.ts b/src/hints/dict/getNextDictKey.test.ts new file mode 100644 index 00000000..179c6d53 --- /dev/null +++ b/src/hints/dict/getNextDictKey.test.ts @@ -0,0 +1,44 @@ +import { describe, expect, test } from 'bun:test'; + +import { Felt } from 'primitives/felt'; +import { Register } from 'vm/instruction'; +import { VirtualMachine } from 'vm/virtualMachine'; + +import { HintName } from 'hints/hintName'; +import { getNextDictKeyParser } from './getNextDictKey'; + +const GET_NEXT_DICT_KEY = { + GetNextDictKey: { + next_key: { + register: 'FP', + offset: 0, + }, + }, +}; + +describe('GetNextDictKey', () => { + test('should properly parse GetNextDictKey hint', () => { + const hint = getNextDictKeyParser.parse(GET_NEXT_DICT_KEY); + expect(hint).toEqual({ + type: HintName.GetNextDictKey, + nextKey: { + register: Register.Fp, + offset: 0, + }, + }); + }); + + test('should properly execute GetNextDictKey hint', () => { + const hint = getNextDictKeyParser.parse(GET_NEXT_DICT_KEY); + const vm = new VirtualMachine(); + vm.memory.addSegment(); + vm.memory.addSegment(); + + const key = new Felt(0n); + vm.squashedDictManager.keys = [key, new Felt(1n)]; + + vm.executeHint(hint); + + expect(vm.memory.get(vm.fp)).toEqual(key); + }); +}); diff --git a/src/hints/dict/getNextDictKey.ts b/src/hints/dict/getNextDictKey.ts new file mode 100644 index 00000000..4380096c --- /dev/null +++ b/src/hints/dict/getNextDictKey.ts @@ -0,0 +1,35 @@ +import { z } from 'zod'; + +import { VirtualMachine } from 'vm/virtualMachine'; + +import { HintName } from 'hints/hintName'; +import { cellRef, CellRef } from 'hints/hintParamsSchema'; + +/** Zod object to parse GetNextDictKey hint */ +export const getNextDictKeyParser = z + .object({ GetNextDictKey: z.object({ next_key: cellRef }) }) + .transform(({ GetNextDictKey: { next_key } }) => ({ + type: HintName.GetNextDictKey, + nextKey: next_key, + })); + +/** + * GetNextDictKey hint + * + * Get the next key to be squashed. + */ +export type GetNextDictKey = z.infer; + +/** + * Get the next key to be squashed. + * + * @param {VirtualMachine} vm - The virtual machine instance + * @param {CellRef} nextKey - Address to store the next key to be squashed. + */ +export const getNextDictKey = (vm: VirtualMachine, nextKey: CellRef) => { + vm.squashedDictManager.popKey(); + vm.memory.assertEq( + vm.cellRefToRelocatable(nextKey), + vm.squashedDictManager.lastKey() + ); +}; diff --git a/src/hints/dict/getSegmentArenaIndex.test.ts b/src/hints/dict/getSegmentArenaIndex.test.ts new file mode 100644 index 00000000..4c09c37c --- /dev/null +++ b/src/hints/dict/getSegmentArenaIndex.test.ts @@ -0,0 +1,93 @@ +import { describe, expect, test } from 'bun:test'; + +import { Felt } from 'primitives/felt'; +import { Relocatable } from 'primitives/relocatable'; +import { Register } from 'vm/instruction'; +import { VirtualMachine } from 'vm/virtualMachine'; +import { segmentArenaHandler } from 'builtins/segmentArena'; + +import { HintName } from 'hints/hintName'; +import { OpType } from '../hintParamsSchema'; +import { getSegmentArenaIndexParser } from './getSegmentArenaIndex'; +import { allocFelt252DictParser } from './allocFelt252Dict'; + +const initSegmentArenaBuiltin = (vm: VirtualMachine) => { + const info = [ + vm.memory.addSegment(segmentArenaHandler), + new Felt(0n), + new Felt(0n), + ]; + const base = vm.memory.addSegment(segmentArenaHandler); + info.map((value, offset) => vm.memory.assertEq(base.add(offset), value)); + return base.add(info.length); +}; + +const ALLOC_FELT252_DICT = { + AllocFelt252Dict: { + segment_arena_ptr: { + Deref: { + register: 'AP', + offset: 0, + }, + }, + }, +}; + +const GET_SEGMENT_ARENA_INDEX = { + GetSegmentArenaIndex: { + dict_end_ptr: { + Deref: { + register: 'AP', + offset: 1, + }, + }, + dict_index: { + register: 'AP', + offset: 2, + }, + }, +}; + +describe('GetSegmentArenaIndex', () => { + test('should properly parse GetSegmentArenaIndex hint', () => { + const hint = getSegmentArenaIndexParser.parse(GET_SEGMENT_ARENA_INDEX); + expect(hint).toEqual({ + type: HintName.GetSegmentArenaIndex, + dictEndptr: { + type: OpType.Deref, + cell: { + register: Register.Ap, + offset: 1, + }, + }, + dictIndex: { + register: Register.Ap, + offset: 2, + }, + }); + }); + + test('should properly execute GetSegmentArenaIndex hint', () => { + const allocHint = allocFelt252DictParser.parse(ALLOC_FELT252_DICT); + const hint = getSegmentArenaIndexParser.parse(GET_SEGMENT_ARENA_INDEX); + const vm = new VirtualMachine(); + vm.memory.addSegment(); + vm.memory.addSegment(); + const arenaPtr = initSegmentArenaBuiltin(vm); + + vm.memory.assertEq(vm.ap, arenaPtr); + vm.executeHint(allocHint); + + const newDictPtr = new Relocatable(4, 0); + vm.memory.assertEq(vm.ap.add(1), newDictPtr); + + vm.executeHint(hint); + + const dict = vm.dictManager.get(newDictPtr.segmentId); + expect(dict).not.toBeUndefined(); + + if (dict) { + expect(vm.memory.get(vm.ap.add(2))).toEqual(dict.id); + } + }); +}); diff --git a/src/hints/dict/getSegmentArenaIndex.ts b/src/hints/dict/getSegmentArenaIndex.ts new file mode 100644 index 00000000..f7311965 --- /dev/null +++ b/src/hints/dict/getSegmentArenaIndex.ts @@ -0,0 +1,49 @@ +import { z } from 'zod'; + +import { VirtualMachine } from 'vm/virtualMachine'; + +import { HintName } from 'hints/hintName'; +import { + resOperand, + ResOperand, + cellRef, + CellRef, +} from 'hints/hintParamsSchema'; + +/** Zod object to parse GetSegmentArenaIndex hint */ +export const getSegmentArenaIndexParser = z + .object({ + GetSegmentArenaIndex: z.object({ + dict_end_ptr: resOperand, + dict_index: cellRef, + }), + }) + .transform(({ GetSegmentArenaIndex: { dict_end_ptr, dict_index } }) => ({ + type: HintName.GetSegmentArenaIndex, + dictEndptr: dict_end_ptr, + dictIndex: dict_index, + })); + +/** + * GetSegmentArenaIndex hint + * + * Assert the index of the dictionary to be squashed in memory + */ +export type GetSegmentArenaIndex = z.infer; + +/** + * Assert the index of the dictionary to be squashed in memory. + * + * @param {VirtualMachine} vm - The virtual machine instance + * @param {ResOperand} dictEndPtr - Pointer to the end of the dictionary. + * @param {CellRef} dictIndex - Address to store the index of the dictionary. + */ +export const getSegmentArenaIndex = ( + vm: VirtualMachine, + dictEndPtr: ResOperand, + dictIndex: CellRef +) => { + const address = vm.getPointer(...vm.extractBuffer(dictEndPtr)); + const dict = vm.getDict(address); + vm.memory.assertEq(vm.cellRefToRelocatable(dictIndex), dict.id); +}; diff --git a/src/hints/dict/initSquashData.test.ts b/src/hints/dict/initSquashData.test.ts new file mode 100644 index 00000000..71ef3acd --- /dev/null +++ b/src/hints/dict/initSquashData.test.ts @@ -0,0 +1,159 @@ +import { describe, expect, test } from 'bun:test'; + +import { Felt } from 'primitives/felt'; +import { Relocatable } from 'primitives/relocatable'; +import { Register } from 'vm/instruction'; +import { VirtualMachine } from 'vm/virtualMachine'; +import { segmentArenaHandler } from 'builtins/segmentArena'; + +import { HintName } from 'hints/hintName'; +import { OpType } from 'hints/hintParamsSchema'; +import { SquashedDictManager } from 'hints/squashedDictManager'; +import { allocFelt252DictParser } from './allocFelt252Dict'; +import { initSquashDataParser } from './initSquashData'; + +const initSegmentArenaBuiltin = (vm: VirtualMachine) => { + const info = [ + vm.memory.addSegment(segmentArenaHandler), + new Felt(0n), + new Felt(0n), + ]; + const base = vm.memory.addSegment(segmentArenaHandler); + info.map((value, offset) => vm.memory.assertEq(base.add(offset), value)); + return base.add(info.length); +}; + +const ALLOC_FELT252_DICT = { + AllocFelt252Dict: { + segment_arena_ptr: { + Deref: { + register: 'AP', + offset: 0, + }, + }, + }, +}; + +const INIT_SQUASH_DATA = { + InitSquashData: { + dict_accesses: { + Deref: { + register: 'AP', + offset: 1, + }, + }, + ptr_diff: { + Deref: { + register: 'AP', + offset: 2, + }, + }, + n_accesses: { + Deref: { + register: 'AP', + offset: 3, + }, + }, + big_keys: { + register: 'AP', + offset: 4, + }, + first_key: { + register: 'AP', + offset: 5, + }, + }, +}; + +describe('InitSquashData', () => { + test('should properly parse InitSquashData hint', () => { + const hint = initSquashDataParser.parse(INIT_SQUASH_DATA); + expect(hint).toEqual({ + type: HintName.InitSquashData, + dictAccesses: { + type: OpType.Deref, + cell: { + register: Register.Ap, + offset: 1, + }, + }, + ptrDiff: { + type: OpType.Deref, + cell: { + register: Register.Ap, + offset: 2, + }, + }, + nAccesses: { + type: OpType.Deref, + cell: { + register: Register.Ap, + offset: 3, + }, + }, + bigKeys: { + register: Register.Ap, + offset: 4, + }, + firstKey: { + register: Register.Ap, + offset: 5, + }, + }); + }); + + test('should properly execute InitSquashData hint', () => { + const allocHint = allocFelt252DictParser.parse(ALLOC_FELT252_DICT); + const hint = initSquashDataParser.parse(INIT_SQUASH_DATA); + const vm = new VirtualMachine(); + vm.memory.addSegment(); + vm.memory.addSegment(); + const arenaPtr = initSegmentArenaBuiltin(vm); + + vm.memory.assertEq(vm.ap, arenaPtr); + vm.executeHint(allocHint); + + const dictPtr = new Relocatable(4, 0); + const key = new Felt(3n); + const values = [ + new Felt(0n), + new Felt(5n), + new Felt(15n), + new Felt(18n), + new Felt(66n), + ]; + + const dict = vm.dictManager.get(dictPtr.segmentId); + const dictAccessSize = 3; + expect(dict).not.toBeUndefined(); + if (!dict) throw new Error('Undefined dict'); + dict.set(key.toString(), values[values.length - 1]); + + values.reduce((prev, curr, i) => { + const index = i - 1; + vm.memory.assertEq(dictPtr.add(index * dictAccessSize), key); + vm.memory.assertEq(dictPtr.add(index * dictAccessSize + 1), prev); + vm.memory.assertEq(dictPtr.add(index * dictAccessSize + 2), curr); + return curr; + }); + + const nAccesses = values.length - 1; + const ptrDiff = nAccesses * dictAccessSize; + vm.memory.assertEq(vm.ap.add(1), dictPtr); + vm.memory.assertEq(vm.ap.add(2), new Felt(BigInt(ptrDiff))); + vm.memory.assertEq(vm.ap.add(3), new Felt(BigInt(nAccesses))); + + vm.executeHint(hint); + + const expectedSquashedDict = new SquashedDictManager(); + expectedSquashedDict.keyToIndices.set(key.toString(), [ + new Felt(3n), + new Felt(2n), + new Felt(1n), + new Felt(0n), + ]); + expectedSquashedDict.keys = [key]; + + expect(vm.squashedDictManager).toEqual(expectedSquashedDict); + }); +}); diff --git a/src/hints/dict/initSquashData.ts b/src/hints/dict/initSquashData.ts new file mode 100644 index 00000000..5a698761 --- /dev/null +++ b/src/hints/dict/initSquashData.ts @@ -0,0 +1,103 @@ +import { z } from 'zod'; + +import { ExpectedFelt } from 'errors/primitives'; +import { InvalidDictAccessesNumber } from 'errors/hints'; + +import { Felt } from 'primitives/felt'; +import { isFelt } from 'primitives/segmentValue'; +import { VirtualMachine } from 'vm/virtualMachine'; + +import { HintName } from 'hints/hintName'; +import { + CellRef, + cellRef, + resOperand, + ResOperand, +} from 'hints/hintParamsSchema'; +import { DICT_ACCESS_SIZE } from 'hints/dictionary'; + +/** Zod object to parse InitSquashData hint */ +export const initSquashDataParser = z + .object({ + InitSquashData: z.object({ + dict_accesses: resOperand, + ptr_diff: resOperand, + n_accesses: resOperand, + big_keys: cellRef, + first_key: cellRef, + }), + }) + .transform( + ({ + InitSquashData: { + dict_accesses, + ptr_diff, + n_accesses, + big_keys, + first_key, + }, + }) => ({ + type: HintName.InitSquashData, + dictAccesses: dict_accesses, + ptrDiff: ptr_diff, + nAccesses: n_accesses, + bigKeys: big_keys, + firstKey: first_key, + }) + ); + +/** + * InitSquashData hint + * + * Initialize the squashing of a dictionary. + */ +export type InitSquashData = z.infer; + +/** + * Initialize the squashing of a dictionary. + * + * @param {VirtualMachine} vm - The virtual machine instance + * @param {ResOperand} dictAccesses - Pointer to the start of the dictionary to be squashed. + * @param {ResOperand} ptrDiff - The difference between dictionary end and start pointers. + * @param {ResOperand} nAccesses - Number of dictonary accesses. + * @param {CellRef} bigKeys - Address to store if the biggest key of the dictionary is above 2 ** 128 or not. + * @param {CellRef} firstKey - Address to store the first key to be squashed. + */ +export const initSquashData = ( + vm: VirtualMachine, + dictAccesses: ResOperand, + ptrDiff: ResOperand, + nAccesses: ResOperand, + bigKeys: CellRef, + firstKey: CellRef +) => { + const address = vm.getPointer(...vm.extractBuffer(dictAccesses)); + + const ptrDiffValue = Number(vm.getResOperandValue(ptrDiff)); + if (ptrDiffValue % DICT_ACCESS_SIZE) + throw new InvalidDictAccessesNumber(ptrDiffValue, DICT_ACCESS_SIZE); + + const nbAccesses = Number(vm.getResOperandValue(nAccesses)); + for (let i = 0; i < nbAccesses; i++) { + const key = vm.memory.get(address.add(i * DICT_ACCESS_SIZE)); + if (!key || !isFelt(key)) throw new ExpectedFelt(key); + vm.squashedDictManager.insert(key, new Felt(BigInt(i))); + } + + vm.squashedDictManager.keyToIndices.forEach((values, key) => { + values.reverse(); + vm.squashedDictManager.keys.push(new Felt(BigInt(key))); + }); + vm.squashedDictManager.keys.sort((a, b) => b.compare(a)); + + vm.memory.assertEq( + vm.cellRefToRelocatable(bigKeys), + vm.squashedDictManager.keys[0] > new Felt(1n << 128n) + ? new Felt(1n) + : new Felt(0n) + ); + vm.memory.assertEq( + vm.cellRefToRelocatable(firstKey), + vm.squashedDictManager.keys[vm.squashedDictManager.keys.length - 1] + ); +}; diff --git a/src/hints/dict/shouldContinueSquashLoop.test.ts b/src/hints/dict/shouldContinueSquashLoop.test.ts new file mode 100644 index 00000000..dafb2867 --- /dev/null +++ b/src/hints/dict/shouldContinueSquashLoop.test.ts @@ -0,0 +1,54 @@ +import { describe, expect, test } from 'bun:test'; + +import { Felt } from 'primitives/felt'; +import { Register } from 'vm/instruction'; +import { VirtualMachine } from 'vm/virtualMachine'; + +import { HintName } from 'hints/hintName'; +import { shouldContinueSquashLoopParser } from './shouldContinueSquashLoop'; + +const SHOULD_CONTINUE_SQUASH_LOOP = { + ShouldContinueSquashLoop: { + should_continue: { + register: 'AP', + offset: 0, + }, + }, +}; + +describe('ShouldContinueSquashLoop', () => { + test('should properly parse ShouldContinueSquashLoop hint', () => { + const hint = shouldContinueSquashLoopParser.parse( + SHOULD_CONTINUE_SQUASH_LOOP + ); + expect(hint).toEqual({ + type: HintName.ShouldContinueSquashLoop, + shouldContinue: { + register: Register.Ap, + offset: 0, + }, + }); + }); + + test.each([ + [[new Felt(4n)], new Felt(0n)], + [[new Felt(13n), new Felt(15n)], new Felt(1n)], + ])( + 'should properly execute ShouldContinueSquashLoop hint', + (values, flag) => { + const hint = shouldContinueSquashLoopParser.parse( + SHOULD_CONTINUE_SQUASH_LOOP + ); + const vm = new VirtualMachine(); + vm.memory.addSegment(); + vm.memory.addSegment(); + + vm.squashedDictManager.keys = [new Felt(0n), new Felt(1n)]; + vm.squashedDictManager.keyToIndices.set('1', values); + + vm.executeHint(hint); + + expect(vm.memory.get(vm.ap)).toEqual(flag); + } + ); +}); diff --git a/src/hints/dict/shouldContinueSquashLoop.ts b/src/hints/dict/shouldContinueSquashLoop.ts new file mode 100644 index 00000000..5a6f9a84 --- /dev/null +++ b/src/hints/dict/shouldContinueSquashLoop.ts @@ -0,0 +1,44 @@ +import { z } from 'zod'; + +import { Felt } from 'primitives/felt'; +import { VirtualMachine } from 'vm/virtualMachine'; + +import { cellRef, CellRef } from 'hints/hintParamsSchema'; +import { HintName } from 'hints/hintName'; + +/** Zod object to parse ShouldContinueSquashLoop hint */ +export const shouldContinueSquashLoopParser = z + .object({ ShouldContinueSquashLoop: z.object({ should_continue: cellRef }) }) + .transform(({ ShouldContinueSquashLoop: { should_continue } }) => ({ + type: HintName.ShouldContinueSquashLoop, + shouldContinue: should_continue, + })); + +/** + * ShouldContinueSquashLoop hint + * + * Assert in memory if there are keys that haven't been squashed yet. + */ +export type ShouldContinueSquashLoop = z.infer< + typeof shouldContinueSquashLoopParser +>; + +/** + * Assert in memory if there are keys that haven't been squashed yet. + * + * This is the opposite of `ShouldSkipSquashLoop`. + * + * @param {VirtualMachine} vm - The virtual machine instance + * @param {CellRef} shouldContinue - Address to store whether the squash loop must continue. + */ +export const shouldContinueSquashLoop = ( + vm: VirtualMachine, + shouldContinue: CellRef +) => { + const flag = + vm.squashedDictManager.lastIndices().length > 1 + ? new Felt(1n) + : new Felt(0n); + + vm.memory.assertEq(vm.cellRefToRelocatable(shouldContinue), flag); +}; diff --git a/src/hints/dict/shouldSkipSquashLoop.test.ts b/src/hints/dict/shouldSkipSquashLoop.test.ts new file mode 100644 index 00000000..e9092daf --- /dev/null +++ b/src/hints/dict/shouldSkipSquashLoop.test.ts @@ -0,0 +1,47 @@ +import { describe, expect, test } from 'bun:test'; + +import { Felt } from 'primitives/felt'; +import { Register } from 'vm/instruction'; +import { VirtualMachine } from 'vm/virtualMachine'; + +import { HintName } from 'hints/hintName'; +import { shouldSkipSquashLoopParser } from './shouldSkipSquashLoop'; + +const SHOULD_SKIP_SQUASH_LOOP = { + ShouldSkipSquashLoop: { + should_skip_loop: { + register: 'AP', + offset: 0, + }, + }, +}; + +describe('shouldSkipSquashLoop', () => { + test('should properly parse ShouldSkipSquashLoop hint', () => { + const hint = shouldSkipSquashLoopParser.parse(SHOULD_SKIP_SQUASH_LOOP); + expect(hint).toEqual({ + type: HintName.ShouldSkipSquashLoop, + shouldSkipLoop: { + register: Register.Ap, + offset: 0, + }, + }); + }); + + test.each([ + [[new Felt(4n)], new Felt(1n)], + [[new Felt(13n), new Felt(15n)], new Felt(0n)], + ])('should properly execute ShouldSkipSquashLoop hint', (values, flag) => { + const hint = shouldSkipSquashLoopParser.parse(SHOULD_SKIP_SQUASH_LOOP); + const vm = new VirtualMachine(); + vm.memory.addSegment(); + vm.memory.addSegment(); + + vm.squashedDictManager.keys = [new Felt(0n), new Felt(1n)]; + vm.squashedDictManager.keyToIndices.set('1', values); + + vm.executeHint(hint); + + expect(vm.memory.get(vm.ap)).toEqual(flag); + }); +}); diff --git a/src/hints/dict/shouldSkipSquashLoop.ts b/src/hints/dict/shouldSkipSquashLoop.ts new file mode 100644 index 00000000..7d588c37 --- /dev/null +++ b/src/hints/dict/shouldSkipSquashLoop.ts @@ -0,0 +1,42 @@ +import { z } from 'zod'; + +import { Felt } from 'primitives/felt'; +import { VirtualMachine } from 'vm/virtualMachine'; + +import { cellRef, CellRef } from 'hints/hintParamsSchema'; +import { HintName } from 'hints/hintName'; + +/** Zod object to parse ShouldSkipSquashLoop hint */ +export const shouldSkipSquashLoopParser = z + .object({ ShouldSkipSquashLoop: z.object({ should_skip_loop: cellRef }) }) + .transform(({ ShouldSkipSquashLoop: { should_skip_loop } }) => ({ + type: HintName.ShouldSkipSquashLoop, + shouldSkipLoop: should_skip_loop, + })); + +/** + * ShouldSkipSquashLoop hint + * + * Assert in memory if there are keys that haven't been squashed yet. + */ +export type ShouldSkipSquashLoop = z.infer; + +/** + * Assert in memory if there are keys that haven't been squashed yet. + * + * This is the opposite of `ShouldContinueSquashLoop`. + * + * @param {VirtualMachine} vm - The virtual machine instance + * @param shouldSkipLoop - Address to store whether the squash loop must be skipped. + */ +export const shouldSkipSquashLoop = ( + vm: VirtualMachine, + shouldSkipLoop: CellRef +) => { + const flag = + vm.squashedDictManager.lastIndices().length > 1 + ? new Felt(0n) + : new Felt(1n); + + vm.memory.assertEq(vm.cellRefToRelocatable(shouldSkipLoop), flag); +}; diff --git a/src/hints/dictionary.test.ts b/src/hints/dictionary.test.ts new file mode 100644 index 00000000..aadf74d3 --- /dev/null +++ b/src/hints/dictionary.test.ts @@ -0,0 +1,28 @@ +import { describe, test, expect } from 'bun:test'; + +import { DictNotFound } from 'errors/dictionary'; + +import { Felt } from 'primitives/felt'; +import { Relocatable } from 'primitives/relocatable'; +import { VirtualMachine } from 'vm/virtualMachine'; +import { Dictionary } from './dictionary'; + +describe('Dictionary', () => { + test('should properly initialize the dict manager', () => { + const vm = new VirtualMachine(); + expect(vm.dictManager.size).toEqual(0); + }); + + test('should properly create a new dictionary', () => { + const vm = new VirtualMachine(); + const address = vm.newDict(); + expect(address).toEqual(new Relocatable(0, 0)); + expect(vm.getDict(address)).toEqual(new Dictionary(new Felt(0n))); + }); + + test('should throw DictNotFound when accessing a non-existing dictionary', () => { + const vm = new VirtualMachine(); + const address = new Relocatable(2, 3); + expect(() => vm.getDict(address)).toThrow(new DictNotFound(address)); + }); +}); diff --git a/src/hints/dictionary.ts b/src/hints/dictionary.ts new file mode 100644 index 00000000..57f35353 --- /dev/null +++ b/src/hints/dictionary.ts @@ -0,0 +1,27 @@ +import { Felt } from 'primitives/felt'; +import { SegmentValue } from 'primitives/segmentValue'; + +export const DICT_ACCESS_SIZE = 3; + +/** Offset to read the key of the entry to update. */ +export const KEY_OFFSET = 3; + +/** + * Offset to read the previous value + * of the entry to read or update. + */ +export const PREV_VALUE_OFFSET = 1; + +/** + * Helper class to implement Cairo dictionaries. + * + * The `id` attribute is needed to keep track + * of the multiple dictionaries and their + * corresponding segment in memory. + */ +export class Dictionary extends Map { + constructor(public readonly id: Felt) { + super(); + this.id = id; + } +} diff --git a/src/hints/hintHandler.ts b/src/hints/hintHandler.ts new file mode 100644 index 00000000..020aeeab --- /dev/null +++ b/src/hints/hintHandler.ts @@ -0,0 +1,128 @@ +import { VirtualMachine } from 'vm/virtualMachine'; + +import { Hint } from './hintSchema'; +import { HintName } from './hintName'; + +import { AllocSegment, allocSegment } from './allocSegment'; +import { + AssertLeFindSmallArcs, + assertLeFindSmallArcs, +} from './assertLeFindSmallArc'; +import { + AssertLeIsFirstArcExcluded, + assertLeIsFirstArcExcluded, +} from './assertLeIsFirstArcExcluded'; +import { + AssertLeIsSecondArcExcluded, + assertLeIsSecondArcExcluded, +} from './assertLeIsSecondArcExcluded'; +import { AllocFelt252Dict, allocFelt252Dict } from './dict/allocFelt252Dict'; +import { + Felt252DictEntryInit, + felt252DictEntryInit, +} from './dict/felt252DictEntryInit'; +import { + Felt252DictEntryUpdate, + felt252DictEntryUpdate, +} from './dict/felt252DictEntryUpdate'; +import { + GetCurrentAccessDelta, + getCurrentAccessDelta, +} from './dict/getCurrentAccessDelta'; +import { + GetCurrentAccessIndex, + getCurrentAccessIndex, +} from './dict/getCurrentAccessIndex'; +import { GetNextDictKey, getNextDictKey } from './dict/getNextDictKey'; +import { + GetSegmentArenaIndex, + getSegmentArenaIndex, +} from './dict/getSegmentArenaIndex'; +import { InitSquashData, initSquashData } from './dict/initSquashData'; +import { + ShouldContinueSquashLoop, + shouldContinueSquashLoop, +} from './dict/shouldContinueSquashLoop'; +import { + ShouldSkipSquashLoop, + shouldSkipSquashLoop, +} from './dict/shouldSkipSquashLoop'; +import { TestLessThan, testLessThan } from './math/testLessThan'; + +/** + * Map hint names to the function executing their logic. + */ +export type HintHandler = Record< + HintName, + (vm: VirtualMachine, hint: Hint) => void +>; + +export const handlers: HintHandler = { + [HintName.AllocFelt252Dict]: (vm, hint) => { + const h = hint as AllocFelt252Dict; + allocFelt252Dict(vm, h.segmentArenaPtr); + }, + [HintName.AllocSegment]: (vm, hint) => { + const h = hint as AllocSegment; + allocSegment(vm, h.dst); + }, + [HintName.AssertLeFindSmallArcs]: (vm, hint) => { + const h = hint as AssertLeFindSmallArcs; + assertLeFindSmallArcs(vm, h.rangeCheckPtr, h.a, h.b); + }, + [HintName.AssertLeIsFirstArcExcluded]: (vm, hint) => { + const h = hint as AssertLeIsFirstArcExcluded; + assertLeIsFirstArcExcluded(vm, h.skipExcludeFirstArc); + }, + [HintName.AssertLeIsSecondArcExcluded]: (vm, hint) => { + const h = hint as AssertLeIsSecondArcExcluded; + assertLeIsSecondArcExcluded(vm, h.skipExcludeSecondArc); + }, + [HintName.Felt252DictEntryInit]: (vm, hint) => { + const h = hint as Felt252DictEntryInit; + felt252DictEntryInit(vm, h.dictPtr, h.key); + }, + [HintName.Felt252DictEntryUpdate]: (vm, hint) => { + const h = hint as Felt252DictEntryUpdate; + felt252DictEntryUpdate(vm, h.dictPtr, h.value); + }, + [HintName.GetCurrentAccessDelta]: (vm, hint) => { + const h = hint as GetCurrentAccessDelta; + getCurrentAccessDelta(vm, h.indexDeltaMinusOne); + }, + [HintName.GetCurrentAccessIndex]: (vm, hint) => { + const h = hint as GetCurrentAccessIndex; + getCurrentAccessIndex(vm, h.rangeCheckPtr); + }, + [HintName.GetNextDictKey]: (vm, hint) => { + const h = hint as GetNextDictKey; + getNextDictKey(vm, h.nextKey); + }, + [HintName.GetSegmentArenaIndex]: (vm, hint) => { + const h = hint as GetSegmentArenaIndex; + getSegmentArenaIndex(vm, h.dictEndptr, h.dictIndex); + }, + [HintName.InitSquashData]: (vm, hint) => { + const h = hint as InitSquashData; + initSquashData( + vm, + h.dictAccesses, + h.ptrDiff, + h.nAccesses, + h.bigKeys, + h.firstKey + ); + }, + [HintName.ShouldContinueSquashLoop]: (vm, hint) => { + const h = hint as ShouldContinueSquashLoop; + shouldContinueSquashLoop(vm, h.shouldContinue); + }, + [HintName.ShouldSkipSquashLoop]: (vm, hint) => { + const h = hint as ShouldSkipSquashLoop; + shouldSkipSquashLoop(vm, h.shouldSkipLoop); + }, + [HintName.TestLessThan]: (vm, hint) => { + const h = hint as TestLessThan; + testLessThan(vm, h.lhs, h.rhs, h.dst); + }, +}; diff --git a/src/hints/hintName.ts b/src/hints/hintName.ts index f490587f..f00a024a 100644 --- a/src/hints/hintName.ts +++ b/src/hints/hintName.ts @@ -1,5 +1,18 @@ /** Name to identify which hint is executed */ export enum HintName { + AllocFelt252Dict = 'AllocFelt252Dict', AllocSegment = 'AllocSegment', + AssertLeFindSmallArcs = 'AssertLeFindSmallArcs', + AssertLeIsFirstArcExcluded = 'AssertLeIsFirstArcExcluded', + AssertLeIsSecondArcExcluded = 'AssertLeIsSecondArcExcluded', + Felt252DictEntryInit = 'Felt252DictEntryInit', + Felt252DictEntryUpdate = 'Felt252DictEntryUpdate', + GetCurrentAccessDelta = 'GetCurrentAccessDelta', + GetCurrentAccessIndex = 'GetCurrentAccessIndex', + GetNextDictKey = 'GetNextDictKey', + GetSegmentArenaIndex = 'GetSegmentArenaIndex', + InitSquashData = 'InitSquashData', + ShouldContinueSquashLoop = 'ShouldContinueSquashLoop', + ShouldSkipSquashLoop = 'ShouldSkipSquashLoop', TestLessThan = 'TestLessThan', } diff --git a/src/hints/hintParamsSchema.test.ts b/src/hints/hintParamsSchema.test.ts index 594bf19c..3cad0aec 100644 --- a/src/hints/hintParamsSchema.test.ts +++ b/src/hints/hintParamsSchema.test.ts @@ -2,13 +2,14 @@ import { describe, expect, test } from 'bun:test'; import { Felt } from 'primitives/felt'; import { Register } from 'vm/instruction'; + import { CellRef, OpType, Operation, - ResOp, + ResOperand, cellRef, - resOp, + resOperand, } from './hintParamsSchema'; describe('hintParamsSchema', () => { @@ -127,7 +128,7 @@ describe('hintParamsSchema', () => { }, }, ], - ])('should properly parse ResOp', (value, expected: ResOp) => { - expect(resOp.parse(value)).toEqual(expected); + ])('should properly parse ResOperand', (value, expected: ResOperand) => { + expect(resOperand.parse(value)).toEqual(expected); }); }); diff --git a/src/hints/hintParamsSchema.ts b/src/hints/hintParamsSchema.ts index aca1044a..c612543b 100644 --- a/src/hints/hintParamsSchema.ts +++ b/src/hints/hintParamsSchema.ts @@ -4,7 +4,7 @@ import { Felt } from 'primitives/felt'; import { Register } from 'vm/instruction'; /** - * Types to distinguish ResOp pattern + * Types to distinguish ResOperand pattern * * Generic patterns: * - Deref: `[register + offset]` @@ -87,8 +87,8 @@ const binOp = z b: object.BinOp.b, })); -/** Zod object to parse a ResOp */ -export const resOp = z.union([deref, doubleDeref, immediate, binOp]); +/** Zod object to parse a ResOperand */ +export const resOperand = z.union([deref, doubleDeref, immediate, binOp]); /** * A CellRef is an object defining a register and an offset @@ -128,5 +128,5 @@ export type Immediate = z.infer; */ export type BinOp = z.infer; -/** A ResOp is either a Deref, DoubleDeref, Immediate or BinOp */ -export type ResOp = z.infer; +/** A ResOperand is either a Deref, DoubleDeref, Immediate or BinOp */ +export type ResOperand = z.infer; diff --git a/src/hints/hintSchema.ts b/src/hints/hintSchema.ts index 6a4899eb..892737db 100644 --- a/src/hints/hintSchema.ts +++ b/src/hints/hintSchema.ts @@ -1,10 +1,39 @@ import { z } from 'zod'; import { allocSegmentParser } from './allocSegment'; -import { testLessThanParser } from './testLessThan'; +import { assertLeFindSmallArcsParser } from './assertLeFindSmallArc'; +import { assertLeIsFirstArcExcludedParser } from './assertLeIsFirstArcExcluded'; +import { assertLeIsSecondArcExcludedParser } from './assertLeIsSecondArcExcluded'; +import { allocFelt252DictParser } from './dict/allocFelt252Dict'; +import { felt252DictEntryInitParser } from './dict/felt252DictEntryInit'; +import { felt252DictEntryUpdateParser } from './dict/felt252DictEntryUpdate'; +import { getCurrentAccessDeltaParser } from './dict/getCurrentAccessDelta'; +import { getCurrentAccessIndexParser } from './dict/getCurrentAccessIndex'; +import { getNextDictKeyParser } from './dict/getNextDictKey'; +import { getSegmentArenaIndexParser } from './dict/getSegmentArenaIndex'; +import { initSquashDataParser } from './dict/initSquashData'; +import { shouldContinueSquashLoopParser } from './dict/shouldContinueSquashLoop'; +import { shouldSkipSquashLoopParser } from './dict/shouldSkipSquashLoop'; +import { testLessThanParser } from './math/testLessThan'; /** Zod object to parse any implemented hints */ -const hint = z.union([allocSegmentParser, testLessThanParser]); +const hint = z.union([ + allocFelt252DictParser, + allocSegmentParser, + assertLeFindSmallArcsParser, + assertLeIsFirstArcExcludedParser, + assertLeIsSecondArcExcludedParser, + felt252DictEntryInitParser, + felt252DictEntryUpdateParser, + getCurrentAccessDeltaParser, + getCurrentAccessIndexParser, + getNextDictKeyParser, + getSegmentArenaIndexParser, + initSquashDataParser, + shouldContinueSquashLoopParser, + shouldSkipSquashLoopParser, + testLessThanParser, +]); /** Zod object to parse an array of hints grouped on a given PC */ export const hintsGroup = z.tuple([z.number(), z.array(hint)]); diff --git a/src/hints/testLessThan.test.ts b/src/hints/math/testLessThan.test.ts similarity index 99% rename from src/hints/testLessThan.test.ts rename to src/hints/math/testLessThan.test.ts index 93b26fac..63ef7de9 100644 --- a/src/hints/testLessThan.test.ts +++ b/src/hints/math/testLessThan.test.ts @@ -3,6 +3,7 @@ import { describe, expect, test } from 'bun:test'; import { Felt } from 'primitives/felt'; import { Register } from 'vm/instruction'; import { VirtualMachine } from 'vm/virtualMachine'; + import { OpType } from 'hints/hintParamsSchema'; import { HintName } from 'hints/hintName'; import { testLessThanParser } from './testLessThan'; diff --git a/src/hints/testLessThan.ts b/src/hints/math/testLessThan.ts similarity index 83% rename from src/hints/testLessThan.ts rename to src/hints/math/testLessThan.ts index 95e478da..d48c0a55 100644 --- a/src/hints/testLessThan.ts +++ b/src/hints/math/testLessThan.ts @@ -2,13 +2,19 @@ import { z } from 'zod'; import { Felt } from 'primitives/felt'; import { VirtualMachine } from 'vm/virtualMachine'; -import { cellRef, resOp, CellRef, ResOp } from 'hints/hintParamsSchema'; + import { HintName } from 'hints/hintName'; +import { + cellRef, + resOperand, + CellRef, + ResOperand, +} from 'hints/hintParamsSchema'; /** Zod object to parse TestLessThan hint */ export const testLessThanParser = z .object({ - TestLessThan: z.object({ lhs: resOp, rhs: resOp, dst: cellRef }), + TestLessThan: z.object({ lhs: resOperand, rhs: resOperand, dst: cellRef }), }) .transform(({ TestLessThan: { lhs, rhs, dst } }) => ({ type: HintName.TestLessThan, @@ -34,8 +40,8 @@ export type TestLessThan = z.infer; */ export const testLessThan = ( vm: VirtualMachine, - lhs: ResOp, - rhs: ResOp, + lhs: ResOperand, + rhs: ResOperand, dst: CellRef ) => { const lhsValue = vm.getResOperandValue(lhs); diff --git a/src/hints/scopeManager.test.ts b/src/hints/scopeManager.test.ts new file mode 100644 index 00000000..76778d36 --- /dev/null +++ b/src/hints/scopeManager.test.ts @@ -0,0 +1,57 @@ +import { describe, expect, test } from 'bun:test'; + +import { CannotExitMainScope, VariableNotInScope } from 'errors/scopeManager'; + +import { Felt } from 'primitives/felt'; +import { ScopeManager } from './scopeManager'; + +describe('ScopeManager', () => { + test('constructor', () => { + const scopeManager = new ScopeManager(); + expect(scopeManager.data.length).toEqual(1); + }); + + test('should properly enter a new scope', () => { + const scopeManager = new ScopeManager(); + scopeManager.enterScope({}); + expect(scopeManager.data.length).toEqual(2); + }); + + test('should properly delete new scopes', () => { + const scopeManager = new ScopeManager(); + scopeManager.enterScope({}); + scopeManager.enterScope({}); + scopeManager.exitScope(); + expect(scopeManager.data.length).toEqual(2); + }); + + test.each([10, 'a', 4n, new Felt(3n), [1, 2, 3], { a: 3, b: [] }])( + 'should properly set variables', + (value: any) => { + const scopeManager = new ScopeManager(); + scopeManager.set('value', value); + expect(scopeManager.get('value')).toEqual(value); + } + ); + + test.each([10, 'a', 4n, new Felt(3n), [1, 2, 3], { a: 3, b: [] }])( + 'should properly delete a defined variable', + (value) => { + const scopeManager = new ScopeManager(); + expect(() => scopeManager.get('value')).toThrow( + new VariableNotInScope('value') + ); + scopeManager.set('value', value); + expect(() => scopeManager.get('value')).not.toThrow(); + scopeManager.delete('value'); + expect(() => scopeManager.get('value')).toThrow( + new VariableNotInScope('value') + ); + } + ); + + test('should throw if trying to delete main scope', () => { + const scopeManager = new ScopeManager(); + expect(() => scopeManager.exitScope()).toThrow(new CannotExitMainScope()); + }); +}); diff --git a/src/hints/scopeManager.ts b/src/hints/scopeManager.ts new file mode 100644 index 00000000..dc611c57 --- /dev/null +++ b/src/hints/scopeManager.ts @@ -0,0 +1,51 @@ +import { VariableNotInScope, CannotExitMainScope } from 'errors/scopeManager'; + +/** + * A dictionary mapping a variable name to its value, + * which can be anything + */ +export type Scope = { + [key: string]: any; +}; + +/** + * A stack of Scope + * Scopes are used to share variables across hints + * Only the latest scope is available + * There is always one scope in the stack, the main scope + */ +export class ScopeManager { + public data: Scope[]; + + constructor() { + this.data = [{}]; + } + + /** Add a new scope to the stack */ + enterScope(newScope: Scope) { + this.data.push(newScope); + } + + /** Pop the stack */ + exitScope() { + if (this.data.length === 1) throw new CannotExitMainScope(); + this.data.pop(); + } + + /** Return the value of variable `name` in the latest scope */ + get(name: string) { + const variable = this.data[this.data.length - 1][name]; + if (variable === undefined) throw new VariableNotInScope(name); + return variable; + } + + /** Set the variable `name` to `value` in the latest scope */ + set(name: string, value: any) { + this.data[this.data.length - 1][name] = value; + } + + /** Delete the variable `name` from the latest scope */ + delete(name: string) { + delete this.data[this.data.length - 1][name]; + } +} diff --git a/src/hints/squashedDictManager.test.ts b/src/hints/squashedDictManager.test.ts new file mode 100644 index 00000000..78dbe324 --- /dev/null +++ b/src/hints/squashedDictManager.test.ts @@ -0,0 +1,58 @@ +import { describe, expect, test } from 'bun:test'; + +import { EmptyKeys } from 'errors/squashedDictManager'; + +import { Felt } from 'primitives/felt'; +import { SquashedDictManager } from './squashedDictManager'; + +const INDICES = [new Felt(0n), new Felt(3n), new Felt(5n), new Felt(1n)]; + +describe('SquashedDictManager', () => { + test('should properly initialize a SquashedDictManager', () => { + const squashedDictManager = new SquashedDictManager(); + expect(squashedDictManager.keys.length).toEqual(0); + expect(squashedDictManager.keyToIndices.size).toEqual(0); + }); + + test('should properly insert new index', () => { + const squashedDictManager = new SquashedDictManager(); + const key = new Felt(2n); + squashedDictManager.keys.push(key); + INDICES.map((index) => squashedDictManager.insert(key, index)); + expect(squashedDictManager.lastIndices()).toEqual(INDICES); + }); + + test('should properly get the last index', () => { + const squashedDictManager = new SquashedDictManager(); + const key = new Felt(2n); + squashedDictManager.keys.push(key); + INDICES.map((index) => squashedDictManager.insert(key, index)); + expect(squashedDictManager.lastIndex()).toEqual( + INDICES[INDICES.length - 1] + ); + }); + + test('should properly pop the last index', () => { + const squashedDictManager = new SquashedDictManager(); + const key = new Felt(2n); + squashedDictManager.keys.push(key); + INDICES.map((index) => squashedDictManager.insert(key, index)); + expect(squashedDictManager.popIndex()).toEqual(INDICES[INDICES.length - 1]); + expect(squashedDictManager.lastIndices()).toEqual( + INDICES.slice(0, INDICES.length - 1) + ); + }); + + test('should properly pop the last key', () => { + const squashedDictManager = new SquashedDictManager(); + const keys = [new Felt(2n), new Felt(5n), new Felt(7n)]; + keys.map((key) => squashedDictManager.keys.push(key)); + expect(squashedDictManager.popKey()).toEqual(keys[keys.length - 1]); + expect(squashedDictManager.keys).toEqual(keys.slice(0, keys.length - 1)); + }); + + test('should throw if there is no keys', () => { + const squashedDictManager = new SquashedDictManager(); + expect(() => squashedDictManager.lastKey()).toThrow(new EmptyKeys()); + }); +}); diff --git a/src/hints/squashedDictManager.ts b/src/hints/squashedDictManager.ts new file mode 100644 index 00000000..cce0a139 --- /dev/null +++ b/src/hints/squashedDictManager.ts @@ -0,0 +1,70 @@ +import { + EmptyIndex, + EmptyIndices, + EmptyKeys, +} from 'errors/squashedDictManager'; + +import { Felt } from 'primitives/felt'; + +/** + * Handle the squashing of dictionaries. + */ +export class SquashedDictManager { + /** Maps the key of a dictionary to its taken values accross the run. */ + public keyToIndices: Map; + /** An array containing the keys that still need to be squashed. */ + public keys: Felt[]; + + constructor() { + this.keyToIndices = new Map(); + this.keys = []; + } + + /** Add `index` to the indices taken by `key` */ + insert(key: Felt, index: Felt) { + const keyStr = key.toString(); + const indices = this.keyToIndices.get(keyStr); + if (!indices) { + this.keyToIndices.set(keyStr, [index]); + } else { + indices.push(index); + } + } + + /** Return the last key of the dictionary. */ + lastKey(): Felt { + const len = this.keys.length; + if (!len) throw new EmptyKeys(); + return this.keys[len - 1]; + } + + /** Remove and return the last key of the dictionary. */ + popKey(): Felt { + const key = this.keys.pop(); + if (!key) throw new EmptyKeys(); + return key; + } + + /** Return the array of indices taken by the last key. */ + lastIndices(): Felt[] { + const key = this.lastKey(); + const indices = this.keyToIndices.get(key.toString()); + if (!indices) throw new EmptyIndices(key); + return indices; + } + + /** Return the last index of the indices taken by the last key. */ + lastIndex(): Felt { + const indices = this.lastIndices(); + const len = indices.length; + if (!len) throw new EmptyIndex(); + return indices[len - 1]; + } + + /** Remove and return the last index of the indices taken by the last key. */ + popIndex(): Felt { + const index = this.lastIndices().pop(); + if (!index) throw new EmptyIndex(); + return index; + } +} diff --git a/src/primitives/felt.test.ts b/src/primitives/felt.test.ts index 77f54a10..e0c4a09d 100644 --- a/src/primitives/felt.test.ts +++ b/src/primitives/felt.test.ts @@ -72,6 +72,12 @@ describe('Felt', () => { const expected = new Felt(1n); expect(result.eq(expected)).toBeTrue(); }); + test('should add a felt and a number properly', () => { + const a = new Felt(10n); + const b = 20; + const expected = new Felt(30n); + expect(a.add(b)).toEqual(expected); + }); }); describe('sub', () => { @@ -89,6 +95,12 @@ describe('Felt', () => { const expected = new Felt(Felt.PRIME - 3n); expect(result.eq(expected)).toBeTrue(); }); + test('should sub a felt and a number properly', () => { + const a = new Felt(10n); + const b = 20; + const expected = new Felt(-10n); + expect(a.sub(b)).toEqual(expected); + }); }); describe('mul', () => { @@ -108,6 +120,12 @@ describe('Felt', () => { ); expect(result.eq(expected)).toBeTrue(); }); + test('should multiply a felt and a number properly', () => { + const a = new Felt(10n); + const b = 20; + const expected = new Felt(200n); + expect(a.mul(b)).toEqual(expected); + }); }); describe('div', () => { @@ -121,4 +139,16 @@ describe('Felt', () => { expect(result).toStrictEqual(a); }); }); + + describe('compare', () => { + test.each([ + [new Felt(1n), new Felt(0n), 1], + [new Felt(1n), new Felt(1n), 0], + [new Felt(0n), new Felt(1n), -1], + [new Felt(-1n), new Felt(-10n), 1], + [new Felt(-1n), new Felt(Felt.PRIME - 1n), 0], + ])('should properly compare two Felt', (a, b, expected) => { + expect(a.compare(b)).toEqual(expected); + }); + }); }); diff --git a/src/primitives/felt.ts b/src/primitives/felt.ts index 024fd92b..e9e19654 100644 --- a/src/primitives/felt.ts +++ b/src/primitives/felt.ts @@ -3,6 +3,7 @@ import { CURVE } from '@scure/starknet'; import { CannotDivideByZero, ExpectedFelt } from 'errors/primitives'; import { SegmentValue, isFelt, isRelocatable } from './segmentValue'; +import { Relocatable } from './relocatable'; export class Felt { private inner: bigint; @@ -29,28 +30,52 @@ export class Felt { this.inner = _inner % Felt.PRIME; } - add(other: SegmentValue): Felt { - if (!isFelt(other)) { - throw new ExpectedFelt(other); + add(other: Felt): Felt; + add(other: number): Felt; + add(other: Relocatable): Relocatable; + add(other: SegmentValue): SegmentValue; + add(other: SegmentValue | number): SegmentValue { + if (isRelocatable(other)) { + return other.add(this); + } + + if (isFelt(other)) { + return new Felt(this.inner + other.inner); } - return new Felt(this.inner + other.inner); + return new Felt(this.inner + BigInt(other)); } - sub(other: SegmentValue): Felt { - if (!isFelt(other)) { + sub(other: Felt): Felt; + sub(other: number): Felt; + sub(other: Relocatable): never; + sub(other: SegmentValue): SegmentValue; + sub(other: SegmentValue | number): SegmentValue { + if (isFelt(other)) { + return new Felt(this.inner - other.inner); + } + + if (isRelocatable(other)) { throw new ExpectedFelt(other); } - return new Felt(this.inner - other.inner); + return new Felt(this.inner - BigInt(other)); } - mul(other: SegmentValue): Felt { - if (!isFelt(other)) { + mul(other: Felt): Felt; + mul(other: number): Felt; + mul(other: Relocatable): never; + mul(other: SegmentValue): SegmentValue; + mul(other: SegmentValue | number): SegmentValue { + if (isRelocatable(other)) { throw new ExpectedFelt(other); } - return new Felt(this.inner * other.inner); + if (isFelt(other)) { + return new Felt(this.inner * other.inner); + } + + return new Felt(this.inner * BigInt(other)); } neg(): Felt { @@ -67,10 +92,26 @@ export class Felt { return this.mul(other.inv()); } + mod(other: SegmentValue): Felt { + if (!isFelt(other)) throw new ExpectedFelt(other); + return new Felt(this.inner % other.inner); + } + eq(other: SegmentValue): boolean { return !isRelocatable(other) && this.inner === other.inner; } + /** + * Compare two Felt. + * + * @param other - The Felt to compare against. + * @returns {number} A positive value if `this > other`, + * a negative value if `this < other` and zero if they're equal. + */ + compare(other: Felt): number { + return this > other ? 1 : this < other ? -1 : 0; + } + sqrt(): Felt { return new Felt(CURVE.Fp.sqrt(this.inner)); } diff --git a/src/primitives/segmentValue.ts b/src/primitives/segmentValue.ts index e1cbeb88..6126c46a 100644 --- a/src/primitives/segmentValue.ts +++ b/src/primitives/segmentValue.ts @@ -17,7 +17,7 @@ export function isRelocatable( segmentValue: SegmentValue ): segmentValue is Relocatable; export function isRelocatable( - segmentValue: Relocatable | number + segmentValue: SegmentValue | number ): segmentValue is Relocatable; export function isRelocatable( segmentValue: SegmentValue | number diff --git a/src/runners/cairoRunner.ts b/src/runners/cairoRunner.ts index a7b7bde1..12b552d2 100644 --- a/src/runners/cairoRunner.ts +++ b/src/runners/cairoRunner.ts @@ -65,9 +65,22 @@ export class CairoRunner { throw new InvalidBuiltins(builtins, this.layout.builtins, layoutName); this.builtins = builtins; - const builtin_stack = builtins - .map(getBuiltin) - .map((builtin) => this.vm.memory.addSegment(builtin)); + const builtin_stack = builtins.map((builtin) => { + const handler = getBuiltin(builtin); + if (builtin === 'segment_arena') { + const initialValues = [ + this.vm.memory.addSegment(handler), + new Felt(0n), + new Felt(0n), + ]; + const base = this.vm.memory.addSegment(handler); + initialValues.map((value, offset) => + this.vm.memory.assertEq(base.add(offset), value) + ); + return base.add(initialValues.length); + } + return this.vm.memory.addSegment(handler); + }); const returnFp = this.vm.memory.addSegment(); this.finalPc = this.vm.memory.addSegment(); const stack = [...builtin_stack, returnFp, this.finalPc]; diff --git a/src/runners/layout.ts b/src/runners/layout.ts index f8eecbaa..d92c2485 100644 --- a/src/runners/layout.ts +++ b/src/runners/layout.ts @@ -14,7 +14,7 @@ export type DilutedPool = { * - rcUnits: Range Check Units. * - publicMemoryFraction: The ratios total memory cells over public memory cells. * - dilutedPool: The diluted pool, used for additionnal checks on Bitwise & Keccak - * - ratios: Dictionnary mapping each builtin name to its ratio. + * - ratios: Dictionary mapping each builtin name to its ratio. * * NOTE: The builtins `segment_arena`, `gas_builtin` and `system` which can be found * in the program builtins are not part of the layouts because they're not proven as such. @@ -38,7 +38,7 @@ export const DEFAULT_DILUTED_POOL: DilutedPool = { }; /** - * Dictionnary containing all the available layouts: + * Dictionary containing all the available layouts: * - plain * - small * - dex diff --git a/src/vm/virtualMachine.test.ts b/src/vm/virtualMachine.test.ts index 99652fe3..3718179b 100644 --- a/src/vm/virtualMachine.test.ts +++ b/src/vm/virtualMachine.test.ts @@ -1,7 +1,7 @@ import { test, expect, describe, spyOn } from 'bun:test'; import { ExpectedFelt, ExpectedRelocatable } from 'errors/primitives'; -import { UnusedRes } from 'errors/virtualMachine'; +import { InvalidBufferResOp, UnusedRes } from 'errors/virtualMachine'; import { Felt } from 'primitives/felt'; import { Relocatable } from 'primitives/relocatable'; @@ -16,6 +16,7 @@ import { Op1Src, } from './instruction'; import { VirtualMachine } from './virtualMachine'; +import { CellRef, Operation, OpType, ResOperand } from 'hints/hintParamsSchema'; const instructions = { InvalidAssertEq: new Instruction( @@ -519,4 +520,221 @@ describe('VirtualMachine', () => { expect(logSpy.mock.results[0].value).toEqual(expectedStr); }); }); + + describe('hint processor', () => { + describe('cellRefToRelocatable', () => { + test.each([ + [{ register: Register.Ap, offset: 0 }, new Relocatable(1, 0)], + [{ register: Register.Ap, offset: 5 }, new Relocatable(1, 5)], + [{ register: Register.Fp, offset: 3 }, new Relocatable(1, 3)], + ])( + 'should properly read the cellRef', + (cellRef: CellRef, expected: Relocatable) => { + const vm = new VirtualMachine(); + expect(vm.cellRefToRelocatable(cellRef)).toEqual(expected); + } + ); + + test('should properly read a Felt', () => { + const vm = new VirtualMachine(); + vm.memory.addSegment(); + vm.memory.addSegment(); + const value = new Felt(10n); + const cellRef: CellRef = { register: Register.Ap, offset: 0 }; + vm.memory.assertEq(vm.ap, value); + expect(vm.getFelt(cellRef)).toEqual(value); + }); + + test('should properly read a Relocatable', () => { + const vm = new VirtualMachine(); + vm.memory.addSegment(); + vm.memory.addSegment(); + const value = new Relocatable(0, 10); + const cellRef: CellRef = { register: Register.Ap, offset: 0 }; + vm.memory.assertEq(vm.ap, value); + expect(vm.getRelocatable(cellRef)).toEqual(value); + }); + + test('should throw when reading a Relocatable as a Felt', () => { + const vm = new VirtualMachine(); + vm.memory.addSegment(); + vm.memory.addSegment(); + const value = new Relocatable(0, 10); + const cellRef: CellRef = { register: Register.Ap, offset: 0 }; + vm.memory.assertEq(vm.ap, value); + expect(() => vm.getFelt(cellRef)).toThrow(new ExpectedFelt(value)); + }); + + test('should throw when reading a Felt as a Relocatable', () => { + const vm = new VirtualMachine(); + vm.memory.addSegment(); + vm.memory.addSegment(); + const value = new Felt(10n); + const cellRef: CellRef = { register: Register.Ap, offset: 0 }; + vm.memory.assertEq(vm.ap, value); + expect(() => vm.getRelocatable(cellRef)).toThrow( + new ExpectedRelocatable(value) + ); + }); + + test('should properly read a pointer', () => { + const vm = new VirtualMachine(); + vm.memory.addSegment(); + vm.memory.addSegment(); + const cell: CellRef = { register: Register.Ap, offset: 0 }; + const offset = new Felt(3n); + const value = new Relocatable(0, 4); + vm.memory.assertEq(vm.ap, value); + expect(vm.getPointer(cell, offset)).toEqual(value.add(offset)); + }); + + test.each([ + [ + { + type: OpType.Deref, + cell: { register: Register.Ap, offset: 0 }, + }, + [{ register: Register.Ap, offset: 0 }, new Felt(0n)], + ], + [ + { + type: OpType.BinOp, + op: Operation.Add, + a: { register: Register.Fp, offset: 2 }, + b: { type: OpType.Immediate, value: new Felt(5n) }, + }, + [{ register: Register.Fp, offset: 2 }, new Felt(5n)], + ], + ])( + 'should properly extract a buffer', + (resOperand: ResOperand, expected) => { + const vm = new VirtualMachine(); + expect(vm.extractBuffer(resOperand)).toEqual( + expected as [CellRef, Felt] + ); + } + ); + + test.each([ + { + type: OpType.DoubleDeref, + cell: { register: Register.Ap, offset: 0 }, + offset: 1, + }, + + { + type: OpType.BinOp, + op: Operation.Add, + a: { register: Register.Fp, offset: 2 }, + b: { + type: OpType.Deref, + cell: { register: Register.Ap, offset: 5 }, + }, + }, + + { + type: OpType.BinOp, + op: Operation.Add, + a: { register: Register.Fp, offset: 2 }, + b: { + type: OpType.Deref, + cell: { register: Register.Ap, offset: 5 }, + }, + }, + { + type: OpType.Immediate, + value: new Felt(4n), + }, + ])( + 'should throw when extracting a buffer with the wrong resOperand', + (resOperand: ResOperand) => { + const vm = new VirtualMachine(); + expect(() => vm.extractBuffer(resOperand)).toThrow( + new InvalidBufferResOp(resOperand) + ); + } + ); + + test.each([ + [ + { + type: OpType.Deref, + cell: { register: Register.Ap, offset: 0 }, + }, + new Felt(3n), + ], + [ + { + type: OpType.DoubleDeref, + cell: { register: Register.Ap, offset: 2 }, + offset: 1, + }, + new Felt(7n), + ], + [ + { + type: OpType.Immediate, + value: new Felt(5n), + }, + new Felt(5n), + ], + [ + { + type: OpType.BinOp, + op: Operation.Add, + a: { register: Register.Fp, offset: 0 }, + b: { type: OpType.Immediate, value: new Felt(5n) }, + }, + new Felt(8n), + ], + [ + { + type: OpType.BinOp, + op: Operation.Mul, + a: { register: Register.Fp, offset: 0 }, + b: { type: OpType.Immediate, value: new Felt(5n) }, + }, + new Felt(15n), + ], + [ + { + type: OpType.BinOp, + op: Operation.Add, + a: { register: Register.Ap, offset: 0 }, + b: { + type: OpType.Deref, + cell: { register: Register.Ap, offset: 1 }, + }, + }, + new Felt(10n), + ], + [ + { + type: OpType.BinOp, + op: Operation.Mul, + a: { register: Register.Ap, offset: 0 }, + b: { + type: OpType.Deref, + cell: { register: Register.Ap, offset: 1 }, + }, + }, + new Felt(21n), + ], + ])( + 'should properly read ResOperand', + (resOperand: ResOperand, expected: Felt) => { + const vm = new VirtualMachine(); + vm.memory.addSegment(); + vm.memory.addSegment(); + const value0 = new Felt(3n); + const value1 = new Felt(7n); + const address = new Relocatable(1, 0); + vm.memory.assertEq(vm.ap, value0); + vm.memory.assertEq(vm.ap.add(1), value1); + vm.memory.assertEq(vm.ap.add(2), address); + expect(vm.getResOperandValue(resOperand)).toEqual(expected); + } + ); + }); + }); }); diff --git a/src/vm/virtualMachine.ts b/src/vm/virtualMachine.ts index b665c3d6..f33ae157 100644 --- a/src/vm/virtualMachine.ts +++ b/src/vm/virtualMachine.ts @@ -1,4 +1,5 @@ import { ExpectedFelt, ExpectedRelocatable } from 'errors/primitives'; +import { UndefinedSegmentValue } from 'errors/memory'; import { InvalidDst, InvalidOp1, @@ -8,13 +9,16 @@ import { UndefinedOp0, InvalidCallOp0Value, UndefinedOp1, + InvalidBufferResOp, } from 'errors/virtualMachine'; +import { DictNotFound } from 'errors/dictionary'; import { InvalidCellRefRegister, UnknownHint } from 'errors/hints'; import { Felt } from 'primitives/felt'; import { Relocatable } from 'primitives/relocatable'; import { SegmentValue, isFelt, isRelocatable } from 'primitives/segmentValue'; import { Memory } from 'memory/memory'; + import { BinOp, CellRef, @@ -23,12 +27,14 @@ import { Immediate, Operation, OpType, - ResOp, + ResOperand, } from 'hints/hintParamsSchema'; -import { allocSegment, AllocSegment } from 'hints/allocSegment'; -import { testLessThan, TestLessThan } from 'hints/testLessThan'; import { Hint } from 'hints/hintSchema'; -import { HintName } from 'hints/hintName'; +import { handlers, HintHandler } from 'hints/hintHandler'; +import { Dictionary } from 'hints/dictionary'; +import { ScopeManager } from 'hints/scopeManager'; +import { SquashedDictManager } from 'hints/squashedDictManager'; + import { ApUpdate, FpUpdate, @@ -63,22 +69,14 @@ export class VirtualMachine { pc: Relocatable; ap: Relocatable; fp: Relocatable; + dictManager: Map; + squashedDictManager: SquashedDictManager; + scopeManager: ScopeManager; trace: TraceEntry[]; relocatedMemory: RelocatedMemory[]; relocatedTrace: RelocatedTraceEntry[]; - /** Maps a hint to its implementation */ - private handlers: Record void> = - { - [HintName.AllocSegment]: (vm, hint) => { - const h = hint as AllocSegment; - allocSegment(vm, h.dst); - }, - [HintName.TestLessThan]: (vm, hint) => { - const h = hint as TestLessThan; - testLessThan(vm, h.lhs, h.rhs, h.dst); - }, - }; + private handlers: HintHandler = handlers; constructor() { this.currentStep = 0n; @@ -90,6 +88,10 @@ export class VirtualMachine { this.pc = new Relocatable(0, 0); this.ap = new Relocatable(1, 0); this.fp = new Relocatable(1, 0); + + this.scopeManager = new ScopeManager(); + this.dictManager = new Map(); + this.squashedDictManager = new SquashedDictManager(); } /** @@ -99,9 +101,7 @@ export class VirtualMachine { * - Run the instruction */ step(hints?: Hint[]): void { - if (hints) { - hints.map((hint) => this.executeHint(hint)); - } + hints?.forEach((hint) => this.executeHint(hint)); const maybeEncodedInstruction = this.memory.get(this.pc); if (maybeEncodedInstruction === undefined) { throw new UndefinedInstruction(this.pc); @@ -481,7 +481,7 @@ export class VirtualMachine { * * NOTE: used in Cairo hints */ - cellRefToRelocatable(cell: CellRef) { + cellRefToRelocatable(cell: CellRef): Relocatable { let register: Relocatable; switch (cell.register) { case Register.Ap: @@ -504,12 +504,10 @@ export class VirtualMachine { * NOTE: used in Cairo hints */ getPointer(cell: CellRef, offset: Felt) { - const address = this.memory.get( - this.cellRefToRelocatable(cell).add(offset) - ); + const address = this.memory.get(this.cellRefToRelocatable(cell)); if (!address || !isRelocatable(address)) throw new ExpectedRelocatable(address); - return address; + return address.add(offset); } /** @@ -539,7 +537,20 @@ export class VirtualMachine { } /** - * Return the value defined by `resOp` + * Return the memory value at the address defined by `cell` + * + * Throw if the value is `undefined` + * + * NOTE: used in Cairo hints + */ + getSegmentValue(cell: CellRef): SegmentValue { + const value = this.memory.get(this.cellRefToRelocatable(cell)); + if (!value) throw new UndefinedSegmentValue(); + return value; + } + + /** + * Return the value defined by `resOperand` * * Generic patterns: * - Deref: `[register + offset]` @@ -552,23 +563,23 @@ export class VirtualMachine { * * NOTE: used in Cairo hints */ - getResOperandValue(resOp: ResOp): Felt { - switch (resOp.type) { + getResOperandValue(resOperand: ResOperand): Felt { + switch (resOperand.type) { case OpType.Deref: - return this.getFelt((resOp as Deref).cell); + return this.getFelt((resOperand as Deref).cell); case OpType.DoubleDeref: - const dDeref = resOp as DoubleDeref; + const dDeref = resOperand as DoubleDeref; const deref = this.getRelocatable(dDeref.cell); const value = this.memory.get(deref.add(dDeref.offset)); if (!value || !isFelt(value)) throw new ExpectedFelt(value); return value; case OpType.Immediate: - return (resOp as Immediate).value; + return (resOperand as Immediate).value; case OpType.BinOp: - const binOp = resOp as BinOp; + const binOp = resOperand as BinOp; const a = this.getFelt(binOp.a); let b: Felt | undefined = undefined; @@ -594,4 +605,54 @@ export class VirtualMachine { } } } + + /** + * Return the address defined at `resOperand`. + * + * This method assume that resOperand points to a Relocatable. + * + * Only Deref and BinOp with Immediate value are valid for extracting a buffer. + * + * NOTE: Used in Cairo hints. + */ + extractBuffer(resOperand: ResOperand): [CellRef, Felt] { + switch (resOperand.type) { + case OpType.Deref: + return [(resOperand as Deref).cell, new Felt(0n)]; + case OpType.BinOp: + const binOp = resOperand as BinOp; + if (binOp.b.type !== OpType.Immediate) + throw new InvalidBufferResOp(resOperand); + return [binOp.a, (binOp.b as Immediate).value]; + default: + throw new InvalidBufferResOp(resOperand); + } + } + + /** + * Creates a new dictionary. + * + * NOTE: used in Cairo hints + */ + newDict(): Relocatable { + const dictAddr = this.memory.addSegment(); + this.dictManager.set( + dictAddr.segmentId, + new Dictionary(new Felt(BigInt(this.dictManager.size))) + ); + return dictAddr; + } + + /** + * Return the dictionary at `address` + * + * Throw if dictionary was not found + * + * NOTE: used in Cairo hints + */ + getDict(address: Relocatable): Dictionary { + const dict = this.dictManager.get(address.segmentId); + if (!dict) throw new DictNotFound(address); + return dict; + } }