Chaya·Edu EDU.CHAYA.DEV
AI 232 · Theory of Computation · Project 02

Project Specification · Project 02 · Fall 2026

Invent a Tiny Language

Make up a language nobody has ever spoken. Write its rules, let Python speak it, then let a stack machine stand guard over it. Thirty minutes, start to finish.

Weight 25% of final grade Due Mon, Nov 9, 2026 · 11:59 PM Submit Brightspace Late −10% per day
01

The mission

In Project 1 you adopted somebody else's machine. This time you make the thing machines exist for: a language of your own. By the end of one sitting, Python will print sentences in a language that did not exist this morning — and a stack machine built from your rules will guard its border.

  1. Invent it. Pizza orders, robot dance routines, potion recipes, tiny arithmetic — anything you like. Four ready-made sparks are in §02, but your own idea beats all of them.
  2. Write its grammar as the Notes 06 dictionary: 3 to 6 rules, and at least one rule that nests — a variable that can show up again inside its own build, directly or through another variable. Nesting is what makes it a language and not a word list.
  3. Hear it speak. Run the script in §03. The generate tool from Notes 06 prints the first strings your rules can build. That printout is the payoff moment of the whole project: the first time your language speaks.
  4. Post a guard. The script then feeds your grammar through grammar_to_pda from Notes 07, and accepts becomes your language's checker: you give it five strings that belong in your language and five that don't, and it prints ten verdicts. All ten right, and the script certifies your language.

One sentence worth keeping: your checker answers yes or no on every input, and it always finishes. That little fact sounds obvious now. In a few weeks it will turn out to be the most important property a program can have.

ha ha! FIRST WORDS
The starter language in the script laughs. Yours can do anything you want.
02

Pick a world to speak about

Your language needs a domain — something its strings mean. Four sparks below; steal the vibe, not the rules. One ground rule for all of them: every symbol is a single character, because that's what the Notes 07 machine code expects. Let the comment next to a rule say that m means mushroom.

The Pizzeria

Orders like pmc! — a p opens the order, toppings pile on one letter at a time, and ! sends it to the oven. No !, no pizza.

letters: p m c !YES: pccm!NO: mp!

The Dance Floor

Robot routines from steps, claps, and spins — where every spin that starts (<) must eventually unspin (>), no matter how many moves happen inside. That's nesting on a dance floor.

letters: s c < >YES: s<sc>sNO: s<c

The Potion Cellar

Recipes that open a cauldron (, add ingredients, and may brew a whole smaller potion inside the big one before closing ). Potions in potions in potions.

letters: ( ) w fYES: (w(f)w)NO: (w(f)

Tiny Arithmetic

Sums like 1+1*1 with parentheses for grouping — the layered E / T / F grammar from Notes 06, shrunk to one digit. The classic, if nothing else sparks.

letters: 1 + * ( )YES: (1+1)*1NO: 1+*1

Here is what a finished grammar looks like — the Pizzeria, written exactly the Notes 06 way. A rule is a list of symbols, a variable is anything that's a key, and the empty list is the empty-string rule:

GRAMMAR = {
    "P": [["p", "T", "!"]],             # an order: pizza, toppings, done
    "T": [["m", "T"], ["c", "T"], []],  # toppings: mushroom or cheese, then more — or stop
}
START = "P"

Run through the script, it speaks: p!, pc!, pm!, pcc!, pcm!, pmc! … and its checker turns away mp!, p!!, and a bare p. Notice the nesting: T builds more T inside itself, so an order holds any number of toppings. And now that it's printed here, the Pizzeria is mine — yours has to be something I haven't seen.

What counts as a language here

3 to 6 rules, written as the Notes 06 dictionary. At least one rule nests: some variable can reappear inside its own build, directly or through another variable. Every symbol is one character. The empty list [] is the empty-string rule — use it wherever a part of a sentence may simply be absent.

03

The script

Copy this whole file into tiny_language.py. The only part you touch is the YOUR PART box at the top. It ships speaking the Laughing Language, so run it once before changing anything — that run shows you exactly what a certified language looks like. Then evict the laughter and move your language in.

# Project 2 -- Invent a Tiny Language.
# Fill in the YOUR PART box, run the file, screenshot everything it prints.
# The starter below is the Laughing Language (ha, haha, hahaha, ...). It runs
# out of the box so you can see a finished run — then replace it with yours.

from collections import deque

# ============================ YOUR PART ============================

GRAMMAR = {
    "S": [["h", "a", "S"], ["h", "a"]],    # S -> haS | ha
}
START = "S"

GOOD = ["ha", "haha", "hahaha", "hahahaha", "hahahahaha"]  # five YES strings
BAD  = ["", "h", "ah", "hah", "ahah"]                      # five NO strings

# ========================= END OF YOUR PART ========================
# Everything below is straight from Notes 06 and 07. Don't edit it.

def generate(grammar, start, max_len):
    """Every string the grammar builds, up to max_len, shortest first."""
    found, seen = set(), set()
    queue = deque([(start,)])
    while queue:
        form = queue.popleft()
        if form in seen:
            continue
        seen.add(form)
        if sum(1 for s in form if s not in grammar) > max_len:
            continue                         # already too long
        spots = [i for i, s in enumerate(form) if s in grammar]
        if not spots:
            found.add("".join(form))         # no variables left: a real string
            continue
        i = spots[0]                         # leftmost variable
        for rule in grammar[form[i]]:
            queue.append(form[:i] + tuple(rule) + form[i + 1:])
    return sorted(found, key=lambda w: (len(w), w))

def accepts(pda, w, max_stack=50):
    """Try every world (state, position, stack), breadth-first."""
    start = (pda["start"], 0, "")
    queue, seen = deque([start]), {start}
    while queue:
        state, pos, stack = queue.popleft()
        if pos == len(w) and state in pda["accept"]:
            return True                          # some world made it
        for frm, read, pop, push, to in pda["moves"]:
            if frm != state:
                continue
            if read and (pos == len(w) or w[pos] != read):
                continue                         # wrong input symbol
            if pop and not stack.endswith(pop):
                continue                         # wrong top of stack
            new_stack = stack[:len(stack) - len(pop)] + push[::-1]
            if len(new_stack) > max_stack:
                continue                         # guard against endless pushing
            world = (to, pos + len(read), new_stack)
            if world not in seen:
                seen.add(world)
                queue.append(world)
    return False                                 # every world died

def grammar_to_pda(grammar, start):
    """Expand-and-match PDA for a grammar with one-character symbols."""
    moves = [("s", "", "", start + "$", "L")]    # start variable on top of $
    terminals = set()
    for var, rules in grammar.items():
        for rule in rules:
            moves.append(("L", "", var, "".join(rule), "L"))   # top is a variable: guess a rule
            terminals.update(s for s in rule if s not in grammar)
    for t in sorted(terminals):
        moves.append(("L", t, t, "", "L"))       # top is a terminal: match the input
    moves.append(("L", "", "$", "", "f"))        # marker on top: every prediction came true
    return {"start": "s", "accept": {"f"}, "moves": moves}

# ------------- the script does the rest: don't touch -------------

print()
print("=" * 52)
print("  YOUR LANGUAGE SPEAKS -- the first strings it builds")
print("=" * 52)
for w in generate(GRAMMAR, START, 10)[:15]:
    print("   " + (w if w else "(the empty string)"))

print()
print("=" * 52)
print("  THE CHECKER -- your grammar, running as a machine")
print("=" * 52)
checker = grammar_to_pda(GRAMMAR, START)
wrong = []
for w, want in [(g, True) for g in GOOD] + [(b, False) for b in BAD]:
    got = accepts(checker, w)
    shown = w if w else "(the empty string)"
    if got != want:
        wrong.append(shown)
    want_s = "YES" if want else "NO"
    got_s = "YES" if got else "NO"
    mark = "ok" if got == want else "<-- WRONG"
    print(f"   {shown:<20} want {want_s:<4} got {got_s:<4} {mark}")

print()
if len(GOOD) != 5 or len(BAD) != 5:
    print("   Hold on: you need exactly five good and five bad strings.")
elif not wrong:
    print("   ALL 10 CORRECT -- LANGUAGE CERTIFIED")
else:
    print("   Not certified yet. Look again at: " + ", ".join(wrong))
print()

Everything below the box is the course's own toolkit, pasted straight from Notes 06 and 07 — generate, accepts, grammar_to_pda — not improved, not decorated. If a verdict comes out wrong, the table points at the exact string, and the last line refuses to certify until all ten are right. That argument between you and your own grammar is where the learning is.

One practical note: if your grammar layers variables the E → E + T way, the checker's max_stack guard is what keeps it finishing — leave it alone and it will always give a verdict.

04

What you hand in

Submit on Brightspace · due Monday, November 9, 2026, 11:59 PM
  1. One Python file — the script with your YOUR PART filled in. (A link to a GitHub repo holding that one file is equally welcome.)
  2. One screenshot — the script's full output: your language speaking, the ten-row verdict table, and the certification line.

To be explicit about what I do not want: no report, no diagram, no write-up, no slides, no extra features. One file, one picture. Late work loses 10% per day, as in the syllabus. Stuck for more than twenty minutes on any step, email me: yair.chaya@liu.edu.

05

How I grade it

Five checkboxes, 20 points each. I go down this exact list, so grade yourself before you submit — check all five and you've earned your 100.

The box students miss is the second-to-last, and almost always because of one lazy bad string. If your five NOs are random letter mash, you never find out your grammar secretly accepts p!!. Near-misses, always.

06

Tiny FAQ

The checker says YES to one of my bad strings. Who's wrong?

The checker isn't — it only knows what your rules told it. Either the grammar builds more than you meant (tighten a rule) or that string secretly belongs in your language after all (move it to the good pile). Deciding which is half the point of the project.

The script seems to hang and never prints anything.

That's the warning from Notes 06: generate only limits letters, so a grammar whose rules swap variables for more variables without ever adding a letter keeps it busy forever. Make sure every rule except the empty-string rule puts at least one real letter on the page, and it finishes fast.

Can a symbol be a whole word, like mushroom?

Not in this project — the Notes 07 machine code keeps its stack as a string of single characters, so every symbol must be one character. Use m and let the comment beside the rule do the talking. (Notes 07 sketches how real languages handle long tokens; that's beyond what's asked here.)