Secret Entrance
Introduction: The Elves have good news and bad news.
Secret Entrance
Introduction:
Hello 2026, this is my first article of the year, and I am here to share my experience on a new exercise I did and have been looking into for a while. Being a software developer I had little to no prior experience in hardware side of the industry. And luckily a bunch of events happened in past couple of months, that pushed me in exploring domains I had never imagined of.
Article:
Read the article for free here : link
Context:
If I remember correctly it was around November when one of my colleague showed me a “chip” that he recently brought which I was not aware of at that time, it immediately caught my interest and I was fascinated as he explained that such “chips”were heavily used by many HFT firms which could send and receive signal much faster than any other modern systems as they are tailor made and are designed to perform a subset of task at speed. The “chip” he showed me was a small FPGA that was recently made available by a Surat based company Vicharak, for a very affordable price. So I immediately placed an order for myself, few days go by, then comes December and as always it was time for our beloved Advent of Code, by this time I had the fpga delivered and I was excited to learn more about it and run my first program, hoping if I could beat my cpu in at-least one task however small it be.
On a weekend as I was going through Vicharak’s repository for a basic introduction and seeking advice for some easy entry level project, I stumbled upon a post on Reddit by Jane Street, regarding their version of AOC i.e. Advent of FPGA, which introduced a new challenge to pick any puzzle in this year’s AOC and to build synthesizable RTL with realistic I/O. So I thought what a great opportunity it was, to learn a new skill while solving AOC! The challenge mentioned about Hardcaml, and provided some basic resources to learn about it therefore I decided to deep dive into the rabbit hole by learning Ocaml first and then use Hardcaml (an Ocaml based HDL).

Problem Statement:
There were multiple problems to choose from, and after going through some of them I decided to go with the “Secret Entrance” (Day 1) as it was a simple one and being new to Ocaml and Hardcaml it felt like a safe bet.
In the problem we were given a list of operations which we had to preform to open the safe, the list involves strings of the form R23, L72, … which represented the direction ( R -> cw, L -> ccw ) and times the lock has to be rotated. The lock had initial position at 50 and the password was the count of how many times we stopped on state 0 while performing operations.
Part B was also very similar we were supposed to count the number of times any we wrapped around in the process.
Solution:
I began by solving the problem in a high level language (C++) first as I was fairly confident with that, I easily solved both the parts within 10 mins and it was time to code it in Ocaml, I went through some starting lectures from professor Michael Ryan here to get myself familiar with syntax of language. I found it very similar to another Functional programming language — Haskell (which I did long time back in college). Once I was confident with the syntax I referred ocaml docs to know more about the data types and library function that were available.
I divided the code in three files, main.ml (responsible for reading input from cin and parsing it), test_bench.ml (responsible for running simulation of synthesised RTL) and hdl.ml (Hardcaml code to synthesise RTL).
STEP I: main.ml
(*main.ml Author: Lakshay Bansal (Talkative-Banana) Date: Jan 10 2026*)
open Printf
let strmod100 inp =
let len = String.length inp in
if len <= 2 then (inp, 0)
else
let additional = int_of_string inp in
let extra = (additional / 100) in
(String.sub inp (len - 2) 2, extra)
let splitter line =
let dir =
if String.starts_with ~prefix:"R" line then 1 else 0
in
let num_str = String.sub line 1 (String.length line - 1) in
let value, wraps = strmod100 num_str in
(dir, int_of_string value, wraps)
let rec read_all acc_times acc_direc acc_wraps =
try
let line = read_line () in
let (t, d, w) = splitter line in
read_all (t :: acc_times) (d :: acc_direc) (w + acc_wraps)
with End_of_file ->
(List.rev acc_times, List.rev acc_direc, acc_wraps)
let () =
let (direc, times, wrap) = read_all [] [] 0 in
let (count, state, over) = Aofpga.Test_bench.password times direc 50 in
printf "count = %d state = %d count2 = %d\n" (count - 1) state (over + wrap)
Here splitter splits the input in two parts
This step involves two parts, splitting the input and parsing it:
PART I: First character of the input is always either R, or L and thus we assigns value 1 or 0 indicate positive/negative rotation.
PART II: Times we need to rotate, since for part A we need to find the eventual state after performing operation we don’t need to perform full rotation beyond 100 but only upto (turn % 100) times (since after every 100th rotation we will end up in same spot).
For part B however, we track how many times we wrapped around therefore we maintain an additional counter additional
We then create two lists out of input where ith element of each list represented direction and times we need to rotate ro perform operation i , and then we passed these equal sized lists (times direc) to password along with initial starting state (50).
password a function defined in test_bench.ml
Step II: hdl.ml
(*hdl.ml
Author: Lakshay Bansal (Talkative-Banana)
Date: Jan 10 2026*)
open Hardcaml
open Signal
let modulo100 (num : Signal.t) =
mux2 (num >=:. 100) (num -:. 100) num
let operation (times : Signal.t) (direc : Signal.t) (state : Signal.t) =
mux2 direc (modulo100 (state +: times)) (modulo100 ((state +:. 100) -: times))
let create_circuit () =
let times = input "times" 32 in
let direc = input "direc" 1 in
let state = input "state" 32 in
let clk = input "clk" 1 in
let rst = input "rst" 1 in
let newstate = operation times direc state in
let spec = Reg_spec.create ~clock:clk ~reset:rst () in
let wrap_fwd = (state +: times) >=:. 100 in
let wrap_bwd = (times >=: state &: (state <>:. 0)) in
let zero = uresize (newstate ==:. 0) 32 in
let pass = mux2 direc (uresize wrap_fwd 32) (uresize wrap_bwd 32) in
let count =
reg_fb spec ~enable:vdd ~width:32
~f:(fun count_prev -> count_prev +: zero)
in
let over =
reg_fb spec ~enable:vdd ~width:32
~f:(fun count_prev -> count_prev +: pass)
in
Circuit.create_exn
~name:"operation"
[ output "newstate" newstate
; output "count" count
; output "over" over]
Next obvious step was to write the functions that will perform these operations.
In Hardcaml we represent information in forms of Signal.
Since our rotation is already < 100 we can have a relatively simple modulo function modulo100 with only ≥ and - operation, as we don’t have complex operations such as % which are easily available in other high level languages.
Another function we define is operation, which as the names suggest performs rotation times times in direc direction from the initial state state. It has mux2 which is very similar to a ternary operator and it performs operation on state depending upon direc as conditional.
And lastly the create_circuit function which defines a bunch of signal which we will use to execute operation, clock, and some registers that will store the result of computation. We also define some additional fields like zero, wrap_ffd, wrap_bwd which will increment our counters depending upon assigned expressions.
For output we create a new circuit and assign three outputs newstate (newstate after single operation), count (times we moved to state 0) and over (to track how many times we wrapped around).
STEP III: test_bench.ml
(*test_bench.ml
Author: Lakshay Bansal (Talkative-Banana)
Date: Jan 10 2026*)
open Hardcaml
open Hardcaml.Cyclesim
open Bits
let password (ltimes : int list) (ldirec : int list) (_state : int)
: (int * int * int) =
let circ = Hdl.create_circuit () in
let sim = create circ in
let times_i = in_port sim "times" in
let direc_i = in_port sim "direc" in
let state_i = in_port sim "state" in
let clk_i = in_port sim "clk" in
let rst_i = in_port sim "rst" in
let over_o = out_port sim "over" in
let count_o = out_port sim "count" in
let newstate_o = out_port sim "newstate" in
(* Reset *)
rst_i := vdd;
clk_i := gnd; cycle sim;
rst_i := gnd;
let rotate itimes idirec istate =
times_i := of_int ~width:32 itimes;
direc_i := of_int ~width:1 idirec;
state_i := of_int ~width:32 istate;
clk_i := vdd; cycle sim;
clk_i := gnd;
to_int !newstate_o
in
let rec operate itimes idirec istate =
match itimes, idirec with
| [], [] -> istate
| t :: ts, d :: ds ->
operate ts ds (rotate t d istate)
| _ -> failwith "Length mismatch"
in
let final_state = operate ltimes ldirec _state in
(to_int !count_o, final_state, to_int !over_o)
Last part that glues everything together is our test_bench which has the function password in it, takes in three lists (ltimes, ldirec and _state) and returns three values count_o (times our state pointed to zero), final_state and over_o (times we wrapped around for part b).
Working of password is pretty straight forward we define the input and output ports that are required by hdl.ml, reset the clk and call recursive function operate which recursively applies rotate function on the state we are at, extracting first element from the lists for times and direction.
And that’s it we finally solved the problem!

Tangent:
Although we have solved the problem I wanted to dive a bit deeper, the input to the problem was pretty small, It was around 4500 operations, these are relatively smaller number of operations which our cpu can do very easily, so I thought instead of this what if our input was bigger, way bigger, how I could we compute the result in reasonable amount of time then.
Surely we can do better than this, but can we?
Turns out we can split our input into multiple chunks, and process each of the chunks in parallel, now it is not as straightforward as the sequential counterpart, assuming we divide our entire input in multiple chunks say [c0, c1, c2, c3, …] we can compute the result of c0 easily as we already know the starting state i.e. 50 but what about part c1, c2, c3 and so on, we don’t know the state we will be on before starting each chunk, fortunately this is where FPGA shines we can do massive amount of work in parallel (“I like to think of it as SIMD on steroids”), so instead of waiting on the starting state for each chunk we precompute result for each starting state in parallel (as there can only me 100 possible starting states), we can do this since the times and direction of rotation is independent of state we are on or started with and we can apply these operation on all possible states at once.
This massively increases our throughput, we are processing 100 different branches for each chunk in parallel, moreover each chunk is also processed in parallel!

This although does not reduces the time complexity of our algorithm which is still O(N), however depending upon the number of chunks we compute in parallel, our time is reduced by the factor of # of chunks.
Implementation:
Implementation is very similar to the sequential example, we have similar functions like strmod100, splitter, read_all_lines, password_parallel, but since the output is much bigger now I modified it so as to read input from a file instead.
We define some constants such as lanes, lane_width and workers, where lanes is the number of states we want to compute in parallel (100 in our case), lane_width is the amount of bits needed to represent state and count of our computation, since state can be up to 100, 8 bits are sufficient, I went with 16 here (and it was 32 in case of sequential), and finally workers which represent number of chunks, we are dividing our input into.
tangent.ml (similar to main.ml)
open Printf
let strmod100 inp =
let len = String.length inp in
if len <= 2 then inp
else String.sub inp (len - 2) 2
let splitter line =
if String.starts_with ~prefix:"R" line then
(1, int_of_string (strmod100 (String.sub line 1 (String.length line - 1))))
else
(0, int_of_string (strmod100 (String.sub line 1 (String.length line - 1))))
let rec read_all_lines lines acc_times acc_direc =
match lines with
| [] -> (List.rev acc_times, List.rev acc_direc)
| line :: rest ->
let (t, d) = splitter line in
read_all_lines rest (t :: acc_times) (d :: acc_direc)
let read_file filename =
let ic = open_in filename in
let len = in_channel_length ic in
let s = really_input_string ic len in
close_in ic;
s
let () =
let filename = Sys.argv.(1) in
let contents = read_file filename in
let lines =
String.split_on_char '\n' contents
|> List.filter (fun s -> String.trim s <> "") in
let (direc, times) = read_all_lines lines [] [] in
let lanes = 100 in
let lane_width = 16 in
let workers = 4 in
let intitial_state = 50 in
let states = List.init (lanes * workers) (fun i -> (i mod lanes)) in
let n = List.length times in
let normalized_times = List.append (List.init (workers - (n mod workers)) (fun _ -> 0)) (times) in
let normalized_direc = List.append (List.init (workers - (n mod workers)) (fun _ -> 0)) (direc) in
let results = Aofpga.Test_bench.password_parallel normalized_times normalized_direc states lanes lane_width workers in
let rec read_entry arr state count =
if state >= (workers * lanes) then
(state, count)
else
let (new_state, new_count) = (List.nth arr state) in
read_entry arr (new_state + lanes) (count + new_count)
in
List.iteri (fun i (state, count) ->
if (i mod lanes == 0) then printf "-------------------------------------------\n";
printf "worker %d: lane %d: count = %d state = %d\n" (i / lanes) (i mod lanes) (count - 1) state;
) results;
printf "-------------------------------------------\n";
let (state, count) = read_entry results intitial_state 0 in
printf "Final Result: count = %d state = %d\n" (count - 1) (state mod lanes)
Note our new password_parallel function returns a list now instead of three value like before, infact it returns a list of 2 x 100 (state, count), result for each starting state. To find final password we can look up in the returned list for each chunk and get the entry corresponding to required starting state, finally adding all the counts to get password.
hdl.ml (parallel implementation)
Without going into much detail as this blog is already to long, our parallel implementation simply extends the input and output vector by a factor of lanes * workers, and all other things remain almost the same.
You can go through implementation if interested.
(* parallel execution *)
(* 3200 bit signals, operate per 32-bit lane *)
(* times workers *)
let operation_parallel (times : Signal.t) (direc : Signal.t) (state : Signal.t) (lanes : int) (lane_width : int) (workers : int) =
let per_lane =
List.init (lanes * workers) (fun i ->
let lo = i * lane_width in
let hi = lo + lane_width - 1 in
let times_i = Signal.select times hi lo in
let state_i = Signal.select state hi lo in
let direc_i = Signal.select direc i i in
let forward = modulo100 (state_i +: times_i) in
let backward = modulo100 ((state_i +:. 100) -: times_i) in
mux2 direc_i forward backward
)
in
Signal.concat_lsb per_lane
let create_circuit_parallel lanes lane_width workers () =
let times = input "times" (lanes * lane_width * workers) in
let direc = input "direc" (lanes * workers) in
let state = input "state" (lanes * lane_width * workers) in
let clk = input "clk" 1 in
let rst = input "rst" 1 in
let newstate = operation_parallel times direc state lanes lane_width workers in
let equal_zero_per_lane =
List.init (lanes * workers) (fun i ->
let lo = i * lane_width in
let hi = lo + lane_width - 1 in
let lane = Signal.select newstate hi lo in
uresize (lane ==:. 0) lane_width
)
|> Signal.concat_lsb in
let spec = Reg_spec.create ~clock:clk ~reset:rst () in
let count =
reg_fb spec ~enable:vdd ~width:(lanes * lane_width * workers)
~f:(fun count_prev -> count_prev +: equal_zero_per_lane)
in
Circuit.create_exn ~name:"operation_parallel"
[ output "newstate" newstate
; output "count" count]
test_bench.ml (parallel implementation)
(*outputs final state and # zeroes on the input list for all parallel states*)
let password_parallel (ltimes : int list) (ldirec : int list) (lstate : int list) (lanes : int) (lane_width : int) (workers : int)
: ((int * int) list) =
let circ = Hdl.create_circuit_parallel lanes lane_width workers () in
let sim = create circ in
let times_i = in_port sim "times" in
let direc_i = in_port sim "direc" in
let state_i = in_port sim "state" in
let clk_i = in_port sim "clk" in
let rst_i = in_port sim "rst" in
let count_o = out_port sim "count" in
let newstate_o = out_port sim "newstate" in
(* Reset *)
rst_i := vdd;
clk_i := gnd; cycle sim;
rst_i := gnd;
let bits_to_int_list ~lanes ~lane_width (b : Bits.t) =
List.init lanes (fun i ->
let lo = i * lane_width in
let hi = lo + lane_width - 1 in
Bits.to_int (Bits.select b hi lo)
) in
let rotate itimes idirec istate workers =
let times_val =
Bits.concat_lsb
(itimes
|> List.map (fun t ->
let bt = Bits.of_int ~width:lane_width t in
List.init lanes (fun _ -> bt))
|> List.flatten)
in
let direc_val =
Bits.concat_lsb
(idirec
|> List.map (fun t ->
let bt = Bits.of_int ~width:1 t in
List.init lanes (fun _ -> bt))
|> List.flatten)
in
let state_val =
Bits.concat_lsb
(List.map
(fun state -> Bits.of_int ~width:lane_width state)
istate)
in
times_i := times_val;
direc_i := direc_val;
state_i := state_val;
clk_i := vdd; cycle sim;
clk_i := gnd;
bits_to_int_list ~lanes:(lanes * workers) ~lane_width:lane_width !newstate_o
in
(*istate is a list now*)
let rec operate itimes idirec istate workers =
match itimes, idirec with
| [], [] -> istate
| t :: ts, d :: ds ->
operate ts ds (rotate t d istate workers) workers
| _ -> failwith "Length mismatch"
in
let chunkify arr w =
let n = List.length arr in
assert ((n mod w) = 0);
let k = n / w in
(* initialize k empty lists *)
let init = List.init k (fun _ -> []) in
let rec aux i acc = function
| [] ->
(* reverse each chunk to restore order *)
List.map List.rev acc
| x :: xs ->
let idx = i mod k in
let acc' =
List.mapi
(fun j l -> if j = idx then x :: l else l)
acc
in
aux (i + 1) acc' xs
in
aux 0 init arr
in
(*list of all values*)
let final_state = operate (chunkify ltimes workers) (chunkify ldirec workers) lstate workers in
let cnt_lst = bits_to_int_list ~lanes:(lanes * workers) ~lane_width:lane_width !count_o in
List.map2 (fun st cnt -> (st, cnt)) final_state cnt_lst
Results:
Example output: When starting with 3 workers worker 1 and 2 shown here
lane represent starting state, and state is final state

Note: On cpu both parallel and sequential will perform same because simulation of parallel implementation will also run sequentially on cpu, but on fpga we will see real improvements.
Conclusion:
I had a lot of fun during this exercise and surely learnt a lot. Thanks to Jane Street, Advent of Code and Vicharak for this unique opportunity, moving forward I will learn more about it and will try to bring something else in my next blog, thanks a lot for reading!
Repository: Link
메타데이터
- post_id
- 74d65a2e5e45
- slug
- secret-entrance-74d65a2e5e45
- url
- https://medium.com/@lakshay21059/secret-entrance-74d65a2e5e45
- canonical_url
- https://medium.com/@lakshay21059/secret-entrance-74d65a2e5e45
- author_url
- https://medium.com/@lakshay21059
- status
- ok
- fetched_at
- 2026-07-10 03:02:36