# 61A Homework 11

Due by 11:59pm on FRIDAY, 8/2

Submission. See the online submission instructions. We have provided a starter file for the questions below.

Readings. Section 3.6 of the online lecture notes.

The Brackulator language shares an evaluator with the Calculator language, but uses a more concise syntax. Instead of using operator names or symbols, Brackulator indicates operations using different kinds of brackets.

(): subtract

<>: multiply

{}: divide

Operand expressions are separated by spaces within these brackets. The following Brackulator expressions are followed by their Calculator equivalents.

[1 2 3]              (+ 1 2 3)

(5)                  (- 5)

[2{4 8}]             (+ 2 (/ 4 8))

<[2{12 6}](3 4 5)>   (* (+ 2 (/ 12 6)) (- 3 4 5))

By solving the following problems, you will implement a parser, brack_read, that returns an expression for the calc_eval evaluator implemented in the Calculator example from lecture.

All of your solutions should be defined in terms of the following dictionaries of bracket types, which configure the parser to supply the correct operator for each bracket:

```BRACKETS = {('[', ']'): '+',
('(', ')'): '-',
('<', '>'): '*',
('{', '}'): '/'}
LEFT_RIGHT = {left:right for left, right in BRACKETS.keys()}
ALL_BRACKETS = set(b for bs in BRACKETS for b in bs)
```

Q1. Implement tokenize, which splits a Brackulator expression into tokens, and raise a ValueError if any token is not a number or a known bracket. Hint: You can first surround each bracket with spaces using line.replace, then split on spaces. Afterward, check each token to ensure that it is legal. The provided coerce_to_number function may prove useful:

```def tokenize(line):
"""Convert a string into a list of tokens.

>>> tokenize('<[2{12.5 6.0}](3 -4 5)>')
['<', '[', 2, '{', 12.5, 6.0, '}', ']', '(', 3, -4, 5, ')', '>']

>>> tokenize('2.3.4')
Traceback (most recent call last):
...
ValueError: invalid token 2.3.4

>>> tokenize('?')
Traceback (most recent call last):
...
ValueError: invalid token ?

>>> tokenize('hello')
Traceback (most recent call last):
...
ValueError: invalid token hello

>>> tokenize('<(GO BEARS)>')
Traceback (most recent call last):
...
ValueError: invalid token GO
"""

def coerce_to_number(token):
"""Coerce a string to a number or return None.

>>> coerce_to_number('-2.3')
-2.3
>>> print(coerce_to_number('('))
None
"""
try:
return int(token)
except (TypeError, ValueError):
try:
return float(token)
except (TypeError, ValueError):
return None
```

Q2. Implement isvalid, which tests whether a prefix of a list of tokens is a well-formed Brackulator expression. A matching right bracket must appear after each left bracket, and if two left brackets appear in sequence, then the matching right bracket of the first must appear after the matching right bracket of the second. Any token that is not a left or right bracket must be a number. For an empty sequence of tokens, you should return False.

Hint: This function is similar to scheme_read from Calculator, but doesn't need to build an expression (that's problem 3):

```def isvalid(tokens):
"""Return whether some prefix of tokens represent a valid Brackulator
expression. Tokens in that expression are removed from tokens as a side
effect.

>>> isvalid(tokenize('([])'))
True
>>> isvalid(tokenize('([]')) # Missing right bracket
False
>>> isvalid(tokenize('[)]')) # Extra right bracket
False
>>> isvalid(tokenize('([)]')) # Improper nesting
False
>>> isvalid(tokenize('')) # No expression
False
>>> isvalid(tokenize('100'))
True
>>> isvalid(tokenize('<(( [{}] [{}] ))>'))
True
>>> isvalid(tokenize('<[2{12 6}](3 4 5)>'))
True
>>> isvalid(tokenize('()()')) # More than one expression is ok
True
>>> isvalid(tokenize('[])')) # Junk after a valid expression is ok
True
"""
```

Q3. Implement brack_read, which returns an expression tree for the first valid Brackulator expression in a list of tokens. The expression tree should contain Calculator operators that correspond to the bracket types. Raise a SyntaxError for any malformed expression. The Pair class and nil object from lecture appear at the bottom of this file.

Once you complete this problem, you can place your homework file in the same directory as scalc.py (and its supporting files), then run read_eval_print_loop to interact with the Brackulator language:

```def brack_read(tokens):
"""Return an expression tree for the first well-formed Brackulator
expression in tokens. Tokens in that expression are removed from tokens as
a side effect.

100
Pair('-', Pair(Pair('+', nil), nil))
(* (+ 2 (/ 12 6)) (- 3 4 5))
>>> brack_read(tokenize('(1)(1)')) # More than one expression is ok
Pair('-', Pair(1, nil))
>>> brack_read(tokenize('[])')) # Junk after a valid expression is ok
Pair('+', nil)

>>> brack_read(tokenize('([]')) # Missing right bracket
Traceback (most recent call last):
...
SyntaxError: unexpected end of line

>>> brack_read(tokenize('[)]')) # Extra right bracket
Traceback (most recent call last):
...
SyntaxError: unexpected )

Traceback (most recent call last):
...
SyntaxError: unexpected )

Traceback (most recent call last):
...
SyntaxError: unexpected end of line
"""
```

Q4. The Python Challenge is a website designed to teach people the many features of the Python Library. Each page of the site is a puzzle that can be solved simply in Python. The solution to each puzzle gives the URL of the next.

To complete your homework, include your solution to puzzle 4 (the one with the picture of a wood carving). You will have to complete puzzles 0, 1, 2, and 3 to reach 4.

http://www.pythonchallenge.com/pc/def/0.html

Some hints:

• Puzzle 1. Try str.maketrans to make a dictionary and str.translate to generate a new string. Letters are listed in the string module.

```>>> 'Borkozoy'.translate(str.maketrans('oz', 'el'))
'Berkeley'
>>> import string
>>> string.ascii_lowercase
'abcdefghijklmnopqrstuvwxyz'
```
• Puzzles 2 & 3. To view the source code of a web page in a browser, use

• Chrome: View > Developer > View Page Source
• Firefox: Tools > Web Developer > Page Source
• Safari: View > View Source

Uppercase letters are also in the string module.

```>>> string.ascii_uppercase
'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
```
• Puzzle 4. Here's an example of fetching the source of a web page in Python. The address below links to an archive of the first WWW site.

```>>> base = 'http://www.w3.org/History/19921103-hypertext/hypertext'
>>> addr = base + '/WWW/TheProject.html'
>>> from urllib.request import urlopen
>>> contents.split('\n')[1]
'<TITLE>The World Wide Web project</TITLE>'
```

As you work on this puzzle, make sure to print the result of each step.

The comments on the puzzle page say: "urllib may help. DON'T TRY ALL NOTHINGS, since it will never end. 400 times is more than enough."

Include your solution to puzzle 4 below:

```from urllib.request import urlopen

def puzzle_4():
"""Return the soluton to puzzle 4."""
```

Support code for Brackulator (from the Calculator example):

```class Pair(object):
"""A pair has two instance attributes: first and second.  For a Pair to be
a well-formed list, second is either a well-formed list or nil.  Some
methods only apply to well-formed lists.

>>> s = Pair(1, Pair(2, nil))
>>> s
Pair(1, Pair(2, nil))
>>> print(s)
(1 2)
>>> len(s)
2
>>> s[1]
2
>>> print(s.map(lambda x: x+4))
(5 6)
"""
def __init__(self, first, second):
self.first = first
self.second = second

def __repr__(self):
return "Pair({0}, {1})".format(repr(self.first), repr(self.second))

def __str__(self):
s = "(" + str(self.first)
second = self.second
while isinstance(second, Pair):
s += " " + str(second.first)
second = second.second
if second is not nil:
s += " . " + str(second)
return s + ")"

def __len__(self):
n, second = 1, self.second
while isinstance(second, Pair):
n += 1
second = second.second
if second is not nil:
raise TypeError("length attempted on improper list")
return n

def __getitem__(self, k):
if k < 0:
raise IndexError("negative index into list")
y = self
for _ in range(k):
if y.second is nil:
raise IndexError("list index out of bounds")
elif not isinstance(y.second, Pair):
raise TypeError("ill-formed list")
y = y.second
return y.first

def map(self, fn):
"""Return a Scheme list after mapping Python function FN to SELF."""
mapped = fn(self.first)
if self.second is nil or isinstance(self.second, Pair):
return Pair(mapped, self.second.map(fn))
else:
raise TypeError("ill-formed list")

class nil(object):
"""The empty list"""

def __repr__(self):
return "nil"

def __str__(self):
return "()"

def __len__(self):
return 0

def __getitem__(self, k):
if k < 0:
raise IndexError("negative index into list")
raise IndexError("list index out of bounds")

def map(self, fn):
return self

nil = nil() # Assignment hides the nil class; there is only one instance
```

To use the following function, you will need to place your homework solution in the same directory as the files from the Calculator Example:

```def read_eval_print_loop():
"""Run a read-eval-print loop for the Brackulator language."""
global Pair, nil