Zulip Chat Archive

Stream: general

Topic: Advent of Code 2024 - Day 3


James Sully (Dec 04 2024 at 02:48):

Here's my solution. Feedback appreciated, and please post your own solution!

This one's pretty janky at the moment, using sort of pseudo parsers. I might clean it up later.
I took it as an opportunity to learn how String.Iterator works. I'll probably rework it with Std.Internal.Parsec.
I am particularly interested in seeing other peoples' solutions who didn't resort to full parser combinators though, since it feels like it ought to be a relatively simple problem, and I still don't feel like I have a good grasp on the string api.

structure MulInstruction where
  a : Nat
  b : Nat
  deriving Repr

def MulInstruction.execute : MulInstruction  Nat
| {a, b} => a * b

def parseString (iter : String.Iterator) (s : String)
  : Option (Unit × String.Iterator) := Id.run do
  let mut sIter := s.iter
  let mut iter' := iter
  while sIter.hasNext do
    if iter'.atEnd || sIter.curr != iter'.curr then
      return .none
    sIter := sIter.next
    iter' := iter'.next
  .some ((), iter')

def parseNat (iter : String.Iterator) : Option (Nat × String.Iterator)
  := Id.run do
  let mut iter' := iter
  while iter'.curr.isDigit do
    if iter'.hasNext then
      iter' := iter'.next
    else
      break
  let subs := iter.extract iter'
  match subs.toNat? with
  | .some n => .some (n, iter')
  | .none => .none

def parseMulInstruction (iter : String.Iterator)
  : Option (MulInstruction × String.Iterator) := do
  let (_, iter₁)  parseString iter "mul("
  let (a, iter₂)  parseNat iter₁
  let (_, iter₃)  parseString iter₂ ","
  let (b, iter₄)  parseNat iter₃
  let (_, iter₅)  parseString iter₄ ")"
  .some ({a,b}, iter₅)

def extractUncorruptedMuls (input : String) : Array MulInstruction := Id.run do
  let mut instructions := #[]
  let mut iter := input.iter
  while iter.hasNext do
    if let .some (mul, iter') := parseMulInstruction iter then
      instructions := instructions.push mul
      iter := iter'
    else
      iter := iter.next
  instructions

def part1 (input : String) : Nat :=
  let instructions := extractUncorruptedMuls input
  instructions.map (·.execute)
  |>.toList
  |>.sum

def extractMulsWithToggle (input : String) : Array MulInstruction := Id.run do
  let mut instructions := #[]
  let mut iter := input.iter
  let mut mulEnabled := true

  while iter.hasNext do
    if let .some (_, iter') := parseString iter "don't()" then
      iter := iter'
      mulEnabled := false
      continue

    if let .some (_, iter') := parseString iter "do()" then
      iter := iter'
      mulEnabled := true
      continue

    if mulEnabled then
      if let .some (mul, iter') := parseMulInstruction iter then
        instructions := instructions.push mul
        iter := iter'
        continue

    iter := iter.next

  instructions

def part2 (input : String) : Nat :=
  let instructions := extractMulsWithToggle input
  instructions.map (·.execute)
    |>.toList
    |>.sum

James Sully (Dec 04 2024 at 03:23):

version 2: turns out Std.Internal.Parsec is extremely straightforward to use, love it. Next I think the extract* functions ought to be refactored into Parsers somehow.

import Std.Internal.Parsec.String

namespace Day03
open Std.Internal.Parsec.String

structure MulInstruction where
  a : Nat
  b : Nat
  deriving Repr

def MulInstruction.execute : MulInstruction  Nat
| {a, b} => a * b

def pmulInstruction : Parser MulInstruction := do
  let _  pstring "mul("
  let a  digits
  let _  pstring ","
  let b  digits
  let _  pstring ")"
  return {a, b}

def extractUncorruptedMuls (input : String) : Array MulInstruction := Id.run do
  let mut instructions := #[]
  let mut iter := input.iter
  while iter.hasNext do
    if let .success iter' mul := pmulInstruction iter then
      instructions := instructions.push mul
      iter := iter'
    else
      iter := iter.next
  instructions

def part1 (input : String) : Nat :=
  let instructions := extractUncorruptedMuls input
  instructions.map (·.execute)
  |>.toList
  |>.sum

def extractMulsWithToggle (input : String) : Array MulInstruction := Id.run do
  let mut instructions := #[]
  let mut iter := input.iter
  let mut mulEnabled := true

  while iter.hasNext do
    if let .success iter' _ := pstring "don't()" iter then
      iter := iter'
      mulEnabled := false
      continue

    if let .success iter' _ := pstring "do()" iter then
      iter := iter'
      mulEnabled := true
      continue

    if mulEnabled then
      if let .success iter' mul := pmulInstruction iter then
        instructions := instructions.push mul
        iter := iter'
        continue

    iter := iter.next

  instructions

def part2 (input : String) : Nat :=
  let instructions := extractMulsWithToggle input
  instructions.map (·.execute)
    |>.toList
    |>.sum

Kyle Miller (Dec 04 2024 at 03:43):

Tip: there are some parsers to consume strings/chars:

def pmulInstruction : Parser MulInstruction := do
  skipString "mul("
  let a  digits
  skipChar ','
  let b  digits
  skipChar ')'
  return {a, b}

Kyle Miller (Dec 04 2024 at 03:44):

One of the benefits of structures is projections:

def MulInstruction.execute (s : MulInstruction) : Nat := s.a * s.b

Kyle Miller (Dec 04 2024 at 03:46):

There's a many combinator:

def extractUncorruptedMuls (input : String) : Array MulInstruction :=
  match many pmulInstruction input.mkIterator with
  | .success _ instructions => instructions
  | .error .. => #[]

James Sully (Dec 04 2024 at 03:50):

Thanks! I'll use skipString and skipChar. I prefer the look of the pattern match for execute personally.

I think that definition of extractUncorruptedMuls is incorrect, because there will be a bunch of garbage data between them. Example input:

xmul(2,4)%&mul[3,7]!@^do_not_mul(5,5)+mul(32,64]then(mul(11,8)mul(8,5))

James Sully (Dec 04 2024 at 03:51):

I'm working on a parser version of extractUncorruptedMuls now, this is what I have so far. I'm trying to figure out how to skip any character:

partial def pmulInstructions : Parser (List MulInstruction) := do
  ( do
      let mul  pmulInstruction
      let muls  pmulInstructions
      return mul :: muls
  ) <|> (
    do
      -- ?? skip single character here
      pmulInstructions
  )

James Sully (Dec 04 2024 at 03:55):

I see in the definition of pchar they use any, but I can't figure out where to import any from.

@[inline]
def pchar (c : Char) : Parser Char := attempt do
  if ( any) = c then pure c else fail s!"expected: '{c}'"

Kyle Miller (Dec 04 2024 at 03:58):

Here's a way to handle the garbage:

partial def extractUncorruptedMuls (input : String) : Array MulInstruction :=
  let parser := do
    many ((some <$> pmulInstruction) <|> (skip *> pure none))
  match parser input.mkIterator with
  | .success _ instructions => instructions.filterMap id
  | .error .. => #[]

It accumulates Option MulInstruction, with garbage giving nones

Kyle Miller (Dec 04 2024 at 03:58):

open Std.Internal.Parsec for the rest of the combinators

James Sully (Dec 04 2024 at 03:59):

ah clever approach, thanks.

Frédéric Dupuis (Dec 04 2024 at 04:00):

BTW, what are the plans for Std.Internal.Parsec? The name sounds like it's meant to be an implementation detail for something else and that we shouldn't be using it.

James Sully (Dec 04 2024 at 04:00):

yeah, i was also curious about that

Kyle Miller (Dec 04 2024 at 04:01):

Good question — all I know is that the release notes mentioned how to migrate to it, so I've been assuming it's ok to use.

Kyle Miller (Dec 04 2024 at 04:04):

(Full disclosure: I may have written said release notes, but it was reviewed!)

Frédéric Dupuis (Dec 04 2024 at 04:05):

In that case, maybe a more inviting name might be in order :-) In any case, for AoC, I've been trying out @François G. Dorais 's parser library.

James Sully (Dec 04 2024 at 04:20):

version 3 of part 1, thanks kyle for the help:

import Std.Internal.Parsec.String
import Std.Internal.Parsec

namespace Day03
open Std.Internal.Parsec.String
open Std.Internal.Parsec (skip many many1 attempt)

structure MulInstruction where
  a : Nat
  b : Nat
  deriving Repr

def MulInstruction.execute : MulInstruction  Nat
| {a, b} => a * b

def pmulInstruction : Parser MulInstruction := do
  skipString "mul("
  let a  digits
  skipChar ','
  let b  digits
  skipChar ')'
  return {a, b}

def pmulInstructions : Parser (Array MulInstruction) :=
  Array.filterMap id <$> many step
  where
    step := some <$> attempt pmulInstruction <|> skip *> pure none

def part1 (input : String) : Nat :=
  let instructions : Array MulInstruction := pmulInstructions.run input
    |>.toOption
    |>.getD #[]
  instructions.map (·.execute)
    |>.toList
    |>.sum

James Sully (Dec 04 2024 at 04:20):

part 2 will be slightly trickier because it's stateful

James Sully (Dec 04 2024 at 04:37):

version 3 of part 2:

partial def pMulInstructionsWithToggle (mulEnabled : Bool)
  : Parser (List MulInstruction) :=
  if mulEnabled then
    pdont <|> pdo <|> mul <|> next <|> done
  else
    pdont <|> pdo <|>         next <|> done
  where
    pdont := attempt <| pstring "don't()" *> pMulInstructionsWithToggle false
    pdo := attempt <| pstring "do()" *> pMulInstructionsWithToggle true
    mul := do
      let mulInstr  attempt pmulInstruction
      let instructions  pMulInstructionsWithToggle mulEnabled
      return mulInstr :: instructions
    next := skip *> pMulInstructionsWithToggle mulEnabled
    done := eof *> pure []

def part2 (input : String) : Nat :=
  let instructions := (pMulInstructionsWithToggle true).run input
    |>.toOption
    |>.getD []
  instructions.map (·.execute)
    |>.sum

Ayhon (Dec 04 2024 at 10:03):

This is my version of Day 3, also using Std.Internal.Parsec which I asked about yesterday. I couldn't get the "parse a mul instruction if it's present, otherwise skip the first character" part to work until I looked at this discussion and saw that I needed to add an attempt to my mul parser, so thanks for that!

import Std.Internal.Parsec
import Std.Internal.Parsec.String

namespace Day3
open Std.Internal.Parsec.String
open Std.Internal.Parsec (skip many many1 attempt)

abbrev Input := String

def parse_input(ls: List String): Input := String.join ls

namespace Part1
  def mul: Parser (Nat × Nat):= do
    skipString "mul("
    let num1 <- digits
    skipChar ','
    let num2 <- digits
    skipString ")"
    /- dbg_trace s!"{num1} × {num2}" -/
    return (num1 , num2)

  def mul_or_move: Parser (Option (Nat × Nat)) := do
    let res <- some <$> (attempt mul) <|> skip *> pure none
    /- dbg_trace s!"{res}" -/
    return res

  def corrupt_memory := do
    let results <- many mul_or_move
    let res := results.toList.reduceOption
    /- dbg_trace s!"results={results}" -/
    /- dbg_trace s!"res={res}" -/
    return res

  def solve(mem: Input): Int :=
    /- dbg_trace mem; -/
    match corrupt_memory mem.iter with
    | .success _ ls => ls.map (fun (a,b) => a*b) |>.sum
    | .error _ err => dbg_trace err; -1

end Part1

namespace Part2
  def pdo := skipString "do()" *> pure true
  def pdont := skipString "don't()" *> pure false
  def do_dont := pdo <|> pdont

  def mul_or_do_dont_or_move :=
    let mulRes := some  Sum.inr
    let dosRes := some  Sum.inl
    mulRes <$> attempt Part1.mul <|>
    dosRes <$> attempt do_dont   <|>
    skip *> pure none

  def corrupt_memory := do
    let results <- many mul_or_do_dont_or_move
    return results.toList.reduceOption

  def computeResult(ls: List (Bool  (Nat × Nat)))(enabled: Bool): Nat :=
    match ls, enabled with
    | .inl en :: rest, _ => computeResult rest en
    | .inr (a,b) :: rest, true => computeResult rest enabled + a * b
    | .inr _ :: rest, false => computeResult rest enabled
    | [], _ => 0

  def solve(mem: Input): Int :=
    match corrupt_memory mem.iter with
    | .success _ tks => computeResult tks true
    | .error _ err => dbg_trace err; -1

end Part2

Ayhon (Dec 04 2024 at 10:06):

Something else that I wasn't expecting is that whenever I try to use >> instead of *>, Lean get's stuck in a typeclass instance problem. I haven't yet understood why this would be the case though. Isn't bind defined for Parser, since we use it in do-notation? Perhaps I'm not understanding the difference between >> and *> correctly :shrug:

Markus Himmel (Dec 04 2024 at 10:11):

Frédéric Dupuis said:

BTW, what are the plans for Std.Internal.Parsec? The name sounds like it's meant to be an implementation detail for something else and that we shouldn't be using it.

Std.Internal.Parsec exists to enable specific use-cases within Lean core. It does not try to be a complete general-purpose parser library. It should be considered an implementation detail and is not a stable API. If it works for your specific use case, then feel free to use it, but we may change it in the future. We will try to make it easy to migrate if that happens, but we won't put as much effort into it as we would for a public/stable API. I haven't looked into the parser library linked above, but that one definitely has the advantage of being written with downstream users in mind.

James Sully (Dec 04 2024 at 12:30):

Ayhon said:

let res := results.toList.reduceOption

Oh, I didn't know about reduceOption, that's handy

James Sully (Dec 04 2024 at 12:32):

I like the approach of sort of parsing it into an IR with explicit representation of the the enabled toggles

James Sully (Dec 04 2024 at 12:33):

makes a lot of sense

François G. Dorais (Dec 04 2024 at 18:47):

Solution using my Parser library:

import Parser.RegEx

open Parser Char

def part1 : TrivialParser Substring Char Nat := do
  let mulRE := RegEx.compile! r"mul\(([1-9][0-9]{,2}),([1-9][0-9]{,2})\)|."
  let mut sum := 0
  while !( test endOfInput) do
    let a  matchStr mulRE
    match a[0]! >>= Substring.toNat?, a[1]! >>= Substring.toNat? with
    | some x, some y => sum := sum + x * y
    | _, _ => continue
  return sum

def part2 : TrivialParser Substring Char Nat := do
  let re := RegEx.compile! r"mul\(([1-9][0-9]{,2}),([1-9][0-9]{,2})\)|(do\(\))|(don't\(\))|."
  let mut state := true
  let mut sum := 0
  while !( test endOfInput) do
    let a  matchStr re
    match a[0]! >>= Substring.toNat?, a[1]! >>= Substring.toNat?, a[2]!, a[3]! with
    | some x, some y, none, none => sum := bif state then sum + x * y else sum
    | none, none, some _, none => state := true
    | none, none, none, some _ => state := false
    | _, _, _, _ => continue
  return sum

def str := include_str "input.txt"

#eval part1.run str
#eval part2.run str

Ayhon (Dec 05 2024 at 11:47):

Didn't know about include_str. It seems pretty useful

Greg Shuflin (Dec 09 2024 at 00:10):

François G. Dorais said:

Solution using my Parser library:

import Parser.RegEx

open Parser Char

def part1 : TrivialParser Substring Char Nat := do
  let mulRE := RegEx.compile! r"mul\(([1-9][0-9]{,2}),([1-9][0-9]{,2})\)|."
  let mut sum := 0
  while !( test endOfInput) do
    let a  matchStr mulRE
    match a[0]! >>= Substring.toNat?, a[1]! >>= Substring.toNat? with
    | some x, some y => sum := sum + x * y
    | _, _ => continue
  return sum

def part2 : TrivialParser Substring Char Nat := do
  let re := RegEx.compile! r"mul\(([1-9][0-9]{,2}),([1-9][0-9]{,2})\)|(do\(\))|(don't\(\))|."
  let mut state := true
  let mut sum := 0
  while !( test endOfInput) do
    let a  matchStr re
    match a[0]! >>= Substring.toNat?, a[1]! >>= Substring.toNat?, a[2]!, a[3]! with
    | some x, some y, none, none => sum := bif state then sum + x * y else sum
    | none, none, some _, none => state := true
    | none, none, none, some _ => state := false
    | _, _, _, _ => continue
  return sum

def str := include_str "input.txt"

#eval part1.run str
#eval part2.run str

I am working on this problem right now, and found your library, and was put off from it by the lack of general documentation. It would be great if you could add a general explanation of how to get started with the library on the README or some other place it links to

Greg Shuflin (Dec 09 2024 at 00:11):

in particular I didn't know that your library had regex functionality, I was just looking for ways that you could match strings against a regex in lean

François G. Dorais (Dec 09 2024 at 04:44):

Indeed, missing docs is one of the reasons why I keep delaying a v1.0.0 release. The examples directory is the closest thing to a getting started guide for now. The regex functionality is new and still experimental; there are no examples for that yet. I'm actually happily surprised it worked for this problem!

Please add an issue to pressure me into writing docs that would help you.

Frédéric Dupuis (Dec 09 2024 at 04:59):

Even running doc-gen on it and hosting the result somewhere would be a big gain in usability I think. I've used it for a few AoC problems as well and it's very nice!

François G. Dorais (Dec 09 2024 at 05:03):

That already exists https://www.dorais.org/lean4-parser/doc/ But I never posted the link anywhere :oops: (Fixed now.)

Frédéric Dupuis (Dec 09 2024 at 05:10):

Ah nice, thanks!

Greg Shuflin (Dec 09 2024 at 06:26):

François G. Dorais said:

Indeed, missing docs is one of the reasons why I keep delaying a v1.0.0 release. The examples directory is the closest thing to a getting started guide for now. The regex functionality is new and still experimental; there are no examples for that yet. I'm actually happily surprised it worked for this problem!

Please add an issue to pressure me into writing docs that would help you.

https://github.com/fgdorais/lean4-parser/issues/57

JJ (Jan 03 2025 at 20:28):

my initial solution to today. i might rewrite it to try and learn monadic parsing? it just so happened there were enough tricks you could do that String.splitOn + Option + functional primitives were able to make it work, though.

def parse_mul (input : String) : List (List Nat) :=
  input.replace "\n" "" |>.splitOn "mul" |>.tail
  |>.map (fun x => (x.splitOn ")").head!.stripPrefix "(")
  |>.map (fun x => (x.splitOn ",").map String.toNat?)
  |>.filter (fun x => x.length == 2 && x.all Option.isSome)
  |>.map (fun x => x.map Option.get!)

def solution (input : String) : String :=
  let p1 := parse_mul input
    |>.foldl (fun acc x => (x.foldl (· * ·) 1) + acc) 0
  let p2 := input.splitOn "do()"
    |>.map (fun x => (x.splitOn "don't()").get! 0)
    |>.foldl (fun acc x => acc ++ x) ""
    |> parse_mul
    |>.foldl (fun acc x => (x.foldl (· * ·) 1) + acc) 0
  s!"{p1}, {p2}"

Last updated: May 02 2025 at 03:31 UTC