Open In Colab

A tool to help write programs#

Attention

Note for instructors

Any class which asks students to write significant programs in 1# would be better off investing in the “Sanity” tool of this section, even though Sanity might push students with no computer experience out of their comfort zone. The key to teaching Sanity is to give examples and to work with people so that they are alert to the strict syntax of Sanity.

The language 1# is intended to be the simplest possible language in which one can discuss the main results of computability theory. One way to make this case is to discuss

  1. Write a program to say “hello world”. That is, write a program that outputs some simple string.

  2. Write a program to send a rocket to the moon. That is, show that scientific computation is possible in the language.

  3. Write a program that can run other programs.

  4. Write a program that outputs itself.

  5. Write a program that trades places with its input. This is a program \(p\) with the property that for all programs \(q\), \([\![p]\!](q)\simeq [\![q]\!](p)\).

We have seen problem 1, and this is basically handled by the write program. When it comes to doing numerical computation, or much of anything else, it is hard to use 1# “out of the box.” That is, it was designed to illustrate results in computability theory but as a result it is very hard for a person to use. This section rectifies matters by providing a tool that makes it easier to write programs in 1#. The program is called sanity.

Here is the tool in a separate notebook.#

This tool makes it easier for someone to organize 1# programs and to write them without having to count lines for all of the forward- and backward-transfer statements.

The concept and the name come from Jon Bowman, who once took my class and felt that construction 1# programs by hand was crazy, and that counting all the 1’s in a long expression “made his eyeballs bleed.”

To start, run the next code cell to install the 1# Python package in your preferred environment. Then run the second code cell to import the functions from the 1# package which are used in this notebook.

!python -m pip install -U setuptools
!python -m pip install -U git+https://github.com/lmoss/onesharp.git@main
from onesharp.interpreter.interpreter import *
Requirement already satisfied: setuptools in /usr/local/lib/python3.12/dist-packages (75.2.0)
Collecting setuptools
  Downloading setuptools-82.0.1-py3-none-any.whl.metadata (6.5 kB)
Downloading setuptools-82.0.1-py3-none-any.whl (1.0 MB)
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 1.0/1.0 MB 14.8 MB/s eta 0:00:00
?25hInstalling collected packages: setuptools
  Attempting uninstall: setuptools
    Found existing installation: setuptools 75.2.0
    Uninstalling setuptools-75.2.0:
      Successfully uninstalled setuptools-75.2.0
ERROR: pip's dependency resolver does not currently take into account all the packages that are installed. This behaviour is the source of the following dependency conflicts.
ipython 7.34.0 requires jedi>=0.16, which is not installed.
torch 2.11.0+cpu requires setuptools<82, but you have setuptools 82.0.1 which is incompatible.
Successfully installed setuptools-82.0.1
Collecting git+https://github.com/lmoss/onesharp.git@main
  Cloning https://github.com/lmoss/onesharp.git (to revision main) to /tmp/pip-req-build-c8fzsrf1
  Running command git clone --filter=blob:none --quiet https://github.com/lmoss/onesharp.git /tmp/pip-req-build-c8fzsrf1
  Resolved https://github.com/lmoss/onesharp.git to commit f228d3607258e12665aa5b5b1fa14e743ed203e7
  Installing build dependencies ... ?25l?25hdone
  Getting requirements to build wheel ... ?25l?25hdone
  Preparing metadata (pyproject.toml) ... ?25l?25hdone
Requirement already satisfied: numpy in /usr/local/lib/python3.12/dist-packages (from onesharp==0.0.0) (2.0.2)
Requirement already satisfied: pandas in /usr/local/lib/python3.12/dist-packages (from onesharp==0.0.0) (2.2.2)
Requirement already satisfied: IPython in /usr/local/lib/python3.12/dist-packages (from onesharp==0.0.0) (7.34.0)
Requirement already satisfied: setuptools>=18.5 in /usr/local/lib/python3.12/dist-packages (from IPython->onesharp==0.0.0) (82.0.1)
Collecting jedi>=0.16 (from IPython->onesharp==0.0.0)
  Downloading jedi-0.20.0-py2.py3-none-any.whl.metadata (23 kB)
Requirement already satisfied: decorator in /usr/local/lib/python3.12/dist-packages (from IPython->onesharp==0.0.0) (4.4.2)
Requirement already satisfied: pickleshare in /usr/local/lib/python3.12/dist-packages (from IPython->onesharp==0.0.0) (0.7.5)
Requirement already satisfied: traitlets>=4.2 in /usr/local/lib/python3.12/dist-packages (from IPython->onesharp==0.0.0) (5.7.1)
Requirement already satisfied: prompt-toolkit!=3.0.0,!=3.0.1,<3.1.0,>=2.0.0 in /usr/local/lib/python3.12/dist-packages (from IPython->onesharp==0.0.0) (3.0.52)
Requirement already satisfied: pygments in /usr/local/lib/python3.12/dist-packages (from IPython->onesharp==0.0.0) (2.20.0)
Requirement already satisfied: backcall in /usr/local/lib/python3.12/dist-packages (from IPython->onesharp==0.0.0) (0.2.0)
Requirement already satisfied: matplotlib-inline in /usr/local/lib/python3.12/dist-packages (from IPython->onesharp==0.0.0) (0.2.2)
Requirement already satisfied: pexpect>4.3 in /usr/local/lib/python3.12/dist-packages (from IPython->onesharp==0.0.0) (4.9.0)
Requirement already satisfied: python-dateutil>=2.8.2 in /usr/local/lib/python3.12/dist-packages (from pandas->onesharp==0.0.0) (2.9.0.post0)
Requirement already satisfied: pytz>=2020.1 in /usr/local/lib/python3.12/dist-packages (from pandas->onesharp==0.0.0) (2025.2)
Requirement already satisfied: tzdata>=2022.7 in /usr/local/lib/python3.12/dist-packages (from pandas->onesharp==0.0.0) (2026.2)
Requirement already satisfied: parso<0.9.0,>=0.8.6 in /usr/local/lib/python3.12/dist-packages (from jedi>=0.16->IPython->onesharp==0.0.0) (0.8.7)
Requirement already satisfied: ptyprocess>=0.5 in /usr/local/lib/python3.12/dist-packages (from pexpect>4.3->IPython->onesharp==0.0.0) (0.7.0)
Requirement already satisfied: wcwidth in /usr/local/lib/python3.12/dist-packages (from prompt-toolkit!=3.0.0,!=3.0.1,<3.1.0,>=2.0.0->IPython->onesharp==0.0.0) (0.8.1)
Requirement already satisfied: six>=1.5 in /usr/local/lib/python3.12/dist-packages (from python-dateutil>=2.8.2->pandas->onesharp==0.0.0) (1.17.0)
Downloading jedi-0.20.0-py2.py3-none-any.whl (4.9 MB)
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 4.9/4.9 MB 52.3 MB/s eta 0:00:00
?25hBuilding wheels for collected packages: onesharp
  Building wheel for onesharp (pyproject.toml) ... ?25l?25hdone
  Created wheel for onesharp: filename=onesharp-0.0.0-py3-none-any.whl size=18563 sha256=65f57e7656278c68d75100d4099fbf1c088cb9b4c8fd76018b7c59451c91870a
  Stored in directory: /tmp/pip-ephem-wheel-cache-daf_fazh/wheels/6d/bf/4c/d8582d2ed4f52867528f81b3093c833c4ecf3cc8b2f78041a7
Successfully built onesharp
Installing collected packages: jedi, onesharp
Successfully installed jedi-0.20.0 onesharp-0.0.0
import inspect
print(inspect.getsource(sanity))
print("\n" + "="*60 + "\n")
print(inspect.getsource(flatten))
print("\n" + "="*60 + "\n")
print(inspect.getsource(resolve))
def sanity(line_list):
  w = [add_placeholder(line) for line in line_list]
  #print("[add_placeholder(line) for line in line_list] is")
  #print(w)
  s1 = [flatten(line) for line in w]
  #print('[flatten(line) for line in w] is')
  #print(s1)
  t1 = [item for sublist in s1 for item in sublist]
  #print('[item for sublist in s1 for item in sublist] is')
  #print(t1)
  n = len(t1)
  #print('len of t1 = ' + str(n))
  u1 = [resolve(i,t1) for i in range(n)] 
  return(unparse(u1))


============================================================

def flatten(tuple):
  b = len(tuple)
  if b > 1 and tuple[1] == "cases":
    n = tuple[2]
    x = ones(n)+'#####'
    return [[tuple[0],x], [tuple[0]+'@1',tuple[3]], [tuple[0]+'@2',tuple[4]], [tuple[0]+'@3',tuple[5]]]
  elif b > 1 and tuple[1]=='add1':
    return([[tuple[0],ones(tuple[2])+'#']])
  elif b > 1 and tuple[1]=='add#':
    return([[tuple[0],ones(tuple[2])+'##']])
  elif b > 1 and tuple[1]=='goto':
    return [[tuple[0],tuple[2]]]
  elif tuple[1][0] in ['1', '#']:
    k = parse(tuple[1])
    m = len(k)
    return([[tuple[0],k[j]] for j in range(m)])


============================================================

def resolve(index,pair_list):
  pair_in_list = pair_list[index]
  #print("pair_in_list " + str(pair_in_list))
  first_char = pair_in_list[1][0]
  #print(first_char)
  if first_char == '1' or first_char == '#':
    return pair_in_list[1]
  elif pair_in_list[1]=='end':
    n = len(pair_list) - index
    return(ones(n)+'###')
  else:
    k = findme(pair_in_list,pair_list)
    if k > index:
      return(ones(k-index)+'###')
    if k < index:
      return(ones(index-k)+'####')

As a way to show what the tool does, we’ll go through an example. Let’s write a program that takes a word

\[ w = w_1 w_2 \cdots w_n \]

in R1 and reverses it.
Here is a flowchart for the program which we’ll write: Our program will work as follows. It processes the letters in \(w\) in order, using a loop that involves cases on R1. At the end of the \(i\)th pass through the loop, we’ll have \(w_{i+1}\cdots w_n\) in R1, and its prefix \(w_{i}\cdots w_2 w_1\) will be in R2, but it will appear there reversed. Thus, after \(n\) passes through the loop, the desired reversal will be in R2. This is how the flowchart ends.

Let’s now consider what happens when we run the \((i+1)\)-st iteration of the loop. We have cases on the first symbol of R1, and we know that this is \(w_{i+1}\). If that first symbol is a \(\one\), we write the same symbol \(\one\) into R3, and then we move R2 onto the end of R3 (emptying out R2) and then R3 is moved back go R2. On the assumption that R2 had \(w_{i}\cdots w_2 w_1\) before this iteration, it now has \(w_{i+1} w_{i}\cdots w_2 w_1\). The same thing happens if the first symbol in R1 was \(\hash\). Finally, if R1 was empty, we would go to the bottom of the flowchart. In this case, R2 would have \(w_{n}\cdots w_2 w_1\). (This is what we want; it is the original input reversed.) And the flowchart indicates that we should move R2 back into R1. So we are done.

With this in mind, have a look at the following Python list called ‘reverse_idea’. It is a list of 8 arrays, and each of them is of a special form.

reverse_idea = [
    ['top', 'cases', 1, 'move_back', 'one_found', 'hash_found'],
    ['one_found','111#'],
    ['goto', 'move_phase'],
    ['hash_found',  '111##'],
    ['goto', 'move_phase'],
    ['move_phase', move(2,3) + move(3,2)],
    ['goto', 'top'],
    ['move_back', move(2,1)]
]

We have here 8 segments. A segment is not the same what we called a line of a \(\one\hash\) program. As the name suggests, a segment corresponds to a sequence of lines. For example, segments 6 and 8 each contain move programs that are bigger than a single instruction. Segments 2, 4 5, and 6 each begin with a label. Labels are strings that other parts of the program could point to. For example, the first segment is a case statement \(\one\hash^5\), and it also contains the information that if R1 is empty, we should go to whichever segmet has the label “move_phase”. (That would be the segment named ‘move_stuff_around’.) The first segment also tells us that if R1 begins with \(\one\) we should (delete is and) go to the segment containing ‘first-is_one’. Note also that ‘goto’ is not a label.

Also, note that a segment is not quite the samething as an item in the flowchart. But this is pretty close.

Workflow#

The workflow in this notebook is that you will need to

(a) think about things deeply enough to make a correct flowchart

(b) draw that flowchart, either with pencil and paper or with some tool (as I did here)

(c) make an “idea” for the desired program using the flowchart, an “idea” with special requirements that we discuss below

(d) call a Python program called sanity on the “idea” to get a \(\one\hash\) program which you can then run.

# example

rev = sanity(reverse_idea)

# This run 'sanity' on 'reverse_idea', calling the result 'rev'.
# We can refer to it in the rest of this notebook by 'rev'.
# For example we can display our new program
rev
'1#####1111111111111111111111###11###111###111#111###111##1###11#####11111 1###111###111##1111####111#11111 1####111#####11111 1###111###11##1111####11#11111 1####1111111111111111111111####11#####11111 1###111###1##1111####1#11111 1####'

Now the program which we just constructed can be run, as usual:

onesharp(rev,['1####'])
'####1'

You can also try your program out using the the Javascript interpreter for 1#.

# Here is a way to write the program 'clear_1':
sanity([
    ['top', 'cases',1,'empty', 'one','hash'],
    ['empty', 'goto', 'end'],
    ['one','goto', 'top'],
    ['hash', 'goto', 'top'],
])
'1#####111###111###111###111###11111####111111####'

The inputs to sanity#

In order to use the tool, we have clarify what we mean by segments and labels.

A segment in sanity is one of the lists in square brackets.

What may go into a segment?

A segment is a Python list: it must be surrounded by square brackects.

A segment can be snippet of 1# code surrounded by quotes.

A segment can be a Python expression like move_3_1 + move_2_1 that denotes a 1# word. (These expressions must be defined before you run sanity, or you will get an error.)

For numbers \(\geq 4\), use move(m,n) instead.

A segment can have ‘add1’ or ‘add#’ followed by a number (without quotes). This number is a register number.

A segment may be the word ‘cases’ followed by a number and then three labels.

A segment may optionally begin with a label like ‘top’, or ‘moveback’, and then it either consists of a snippet of \(\one\hash\) code surrounded by single quotes, or a Python expression that denotes a snippet of \(\one\hash\) code

Important note

Please note: to move or copy registers with numbers \(\geq 4\), you should do move(m,n) rather than move_m_n.

labels

A label may be a word of English, and it may have the underscore symbol, but it should not have spaces. It must be surrounded by quotes.

A label must not begin with ‘1’ or ‘#, and it must not be one of the strings ‘goto’, ‘end’, ‘add1’, or ‘add#’.

Labels are optional, except a “cases” instruction must have a number and then three labels. The number is for a register. So “cases on register 17” would correspond to a segment with the number 17 as its third entry.

Another label which may be used is ‘goto’. A use of ‘goto’ must be followed either by a label or the word ‘end’.

Every label used inside a ‘cases’ or ‘goto’ statement must be the first label in some segment. Otherwise, sanity will raise an error.

Here is another example, another derivation of the “diag” program which we have seen earlier.

dg_idea = [
    ['top','cases',1,'empty', 'one','hash'],
    ['empty', 'goto', 'moveback'],
    ['one', 'add1', 2],
    ['111#111##'],
    ['goto', 'top'],
    ['hash', 'add#', 2],
    ['111#111##111##'],
    ['goto', 'top'],
    ['moveback', move(3,1)+move(2,1)]
]
dg = sanity(diag_idea)
onesharp(dg,['11#'])
'1#1#1##11#'

Examples of segments:#

['top','cases',1,'empty', 'one_found','sharp_found'],
['empty', 'goto', 'moveback'],
['one_found', 'add1', 2],
['111#111##'],
['goto', 'top'],
['hash_found', 'add#', 2],
['111#111##111##'],
['goto', 'end'],
['moveback', move(3,1)+move(2,1)]

How does Sanity work?#

We aren’t going to discuss this at length (now), but you can read the Python code for it by examining the input box at the top. Eventually this code will be documented well-enough so that it can be read. The overall idea is to work with parsed programs, the Python lists that we obtained with the function parse that we saw earlier. The program sanity consists of manipulations of parses, followed by an application of unparse which returns us to a 1# program.

Further programs#

# This code cell contains a Sane program which multiplies the contents of
#   registers one and two and stores the product back into register one

sane_multiply = [
  [move(1,4)],
  ['1##'],
  [copy(2,5,10)],
  ['111##'],
  [copy(3,6,10)],
  [compare(2,3)],
  ['multiply_loop', 'cases', 2, 'empty', 'one', 'sharp'],
    ['empty', copy(4,7,10)],
      [add(1,4,10)],
      [move(7,4)],
      [copy(5,2,10)],
      [successor(6,10)],
      [copy(6,3,10)],
      [compare(2,3)],
      ['goto', 'multiply_loop'],
    ['one', 'goto', 'epilogue'],
    ['sharp', 'goto', 'end'], # We shouldn't reach here because cmp shold never
                              #   write sharp into register two
  ['epilogue', clear(4)],
    [clear(5)],
    [clear(6)]
]
onesharp_multiply = sanity(sane_multiply)
onesharp(onesharp_multiply, ['11', '1#1']) # 11*1#1 = 1111 <==> 3*5 = 15
'1111'
# This code cell contains a Sane program which exponentiates the contents of
#   register one to the power of the contents of register two and stores the
#   result back into register one

sane_exponentiate = [
  [move(1,14)],
  [ones(11)+'#'],
  [move(2,12)],
  [copy(12,15,20)],
  [ones(13)+'##'],
  [copy(13,16,20)],
  [compare(12,13)],
  ['exponentiate_loop', 'cases', 12, 'empty', 'one', 'sharp'],
    ['empty', move(11,1)],
      [copy(14,2,20)],
      [onesharp_multiply],
      [move(1,11)],
      [copy(15,12,20)],
      [successor(16,20)],
      [copy(16,13,20)],
      [compare(12,13)],
      ['goto', 'exponentiate_loop'],
    ['one', 'goto', 'epilogue'],
    ['sharp', 'goto', 'end'], # We shouldn't reach here because cmp shold never
                              #   write sharp into register two
  ['epilogue', move(11,1)],
    [clear(14)],
    [clear(15)],
    [clear(16)]
]
onesharp_exponentiate = sanity(sane_exponentiate)
onesharp(onesharp_exponentiate, ['11', '1#1']) # 11^1#1 = 11##1111 <==> 3^5 = 243
'11##1111'
onesharp(onesharp_exponentiate, ['11', '###1']) # 11^###1 = 1####1#11##11 <==> 3^8 = 6561
# 6561 base 2 is 1100110100001
'1####1#11##11'
pre_pred = [
   ['top', 'cases', 1, 'first_end', 'first_one', 'first_hash'],
   ['first_one', 'cases', 1, 'hash_is_it', 'returnA','returnB'],
   ['hash_is_it', '1##'],
   ['goto', 'second_end'],
   ['returnA', '11#11#'],
   [move(1,2) + move(2,1)],
   ['goto', 'end'],
   ['returnB', '11#11##'],
   [move(1,2) + move(2,1)],
   ['goto', 'end'],
   ['first_hash', 'cases', 1, 'first_end', 'hash_one', 'hash_hash'],
   ['hash_one','11##'],
   ['hash_hash','1###'],
   ['second_end', '1111#'],
   ['goto', 'end'],
   ['first_end', '111#']
 ]
onesharp(sanity(pre_pred), ['#1'])
This is undefined.
The register contents at the end are shown below.
  contents
1
2 #
3
4 1

Here is a derivation of a progam that takes the predecessor of a number written in backwards binary (bb) notation.

pred = [
     ['top','cases', 1, 'a', 'b','c'],
     ['a', 'goto', 'end'],
     ['b', 'cases', 1, 'oe', 'oo', 'oh'],
     ['oe', '1##'],
     ['goto', 'end'],
     ['oo', '11#11#'+move(1,2)+move(2,1)],
     ['goto', 'main'],
     ['oh', '11#11##'+move(1,2)+move(2,1)],
     ['goto', 'main'],
     ['c', 'cases', 1, 'he', 'ho', 'hh'],
     ['he', '1##'],
     ['goto', 'end'],
     ['ho', '11##11#'+move(1,2)+move(2,1)],
     ['goto', 'main'],
     ['hh', '11##11##'+move(1,2)+move(2,1)],
     ['goto', 'main'],
     ['main', 'cases', 1, 'empty', 'one','hash'],
     ['empty', move(2,1)],
     ['goto', 'end'],
     ['one', '11##'],
     [move(1,2) + move(2,1)],
     ['goto', 'end'],
     ['hash', '11#'],
     ['borrowing', 'cases', 1, 'borrowing_empty', 'borrowing_one', 'borrowing_hash'],
     ['borrowing_empty', move(2,1)],
     ['goto', 'end'],
     ['borrowing_one', '11##'],
     [move(1,2) + move(2,1)],
     ['goto', 'end'],
     ['borrowing_hash', '11#'],
     ['goto','borrowing']
]

Exercises#

Exercise 55

Write a program which takes a program \(p\) as input and gives the first instruction of \(p\).

Exercise 56

For any program \(p\), \(|p|\) denotes the number of instructions in \(p\).

Write a program to compute the function \(p\mapsto |p|\), using unary representation of numbers.

An issue to think about

You probabily noticed that the program sanity was written in Python, a high-level programming language. This is partly because 1# does not have any support for the Roman alphabet, and so to even to read the tags one would need to do some coding. But even more importantly, a high-level programming language has things that enable a person to write programs: attention to structure, a type system, conventions about handling variables, and more. This brings us to a question that will crop up at several points: can one really do everthing in 1#, or are high-level tools necessary? This is not a mathematical question, and it might be more of an opinion question than anything else. We bring it up here so that you can keep it in mind as you use high-level tools in connection with a very low-level language.

# 1# idea tool (Colab)
#
# Requires:
# !python -m pip install -U git+https://github.com/lmoss/onesharp.git@main
# from onesharp.interpreter.interpreter import *

import ast
import re
from dataclasses import dataclass
from typing import List

import ipywidgets as widgets
from IPython.display import display

RESERVED = {"cases", "goto", "end"}

CELL_W = "94px"
DEL_W = "32px"
GRID_W = "520px"

# ---------- helpers ----------

def is_label(s):
    return bool(re.fullmatch(r"[A-Za-z][A-Za-z0-9_]*", s or "")) and s not in RESERVED

def is_cases(s):
    return bool(re.fullmatch(r"cases\s+(?:R)?\d+", s or "", re.I))

def cases_num(s):
    m = re.fullmatch(r"cases\s+(?:R)?(\d+)", s or "", re.I)
    return int(m.group(1)) if m else None

def parse_defs(txt):
    d = {}
    for line in (txt or "").splitlines():
        if "=" in line:
            k, v = line.split("=", 1)
            k = k.strip()
            v = v.strip()
            try:
                val = ast.literal_eval(v)
                if isinstance(val, str):
                    d[k] = val
            except:
                pass
    return d

def valid_expr(expr, defs):
    if re.fullmatch(r"[1#]+", expr or ""):
        return True, ""
    if expr in defs:
        return True, ""
    try:
        tree = ast.parse(expr, mode="eval")
    except:
        return False, "invalid syntax"

    allowed = set(defs) | {"move", "copy"}

    for n in ast.walk(tree):
        if isinstance(n, ast.Name) and n.id not in allowed:
            return False, f"variable '{n.id}' is not defined"

    return True, ""

# ---------- row ----------

@dataclass
class Row:
    boxes: list
    delete: widgets.Button
    widget: widgets.HBox

    def vals(self):
        return [b.value.strip() for b in self.boxes]

    def empty(self):
        return not any(self.vals())

# ---------- tool ----------

class IdeaTool:

    def __init__(self):
        self.add = widgets.Button(description="Add segment")
        self.load = widgets.Button(description="Load example")
        self.clear = widgets.Button(description="Clear input")
        self.validate_btn = widgets.Button(description="Validate", button_style="primary")

        self.strings = widgets.Textarea(
            layout=widgets.Layout(width=GRID_W, height="80px")
        )

        self.rows = []
        self.grid = widgets.VBox(layout=widgets.Layout(gap="0px"))
        self.out = widgets.Output()

        self.add.on_click(self.add_row)
        self.load.on_click(self.load_example)
        self.clear.on_click(lambda _: self.reset())
        self.validate_btn.on_click(self.run)

        self.reset()
        self.ui = self.build()

    # ---------- UI helpers ----------

    def cell(self, v=""):
        t = widgets.Text(
            value=v,
            layout=widgets.Layout(width=CELL_W, height="26px")
        )

        def highlight(change=None):
            val = t.value.strip()
            if val == "end":
                t.add_class("end-box")
            else:
                t.remove_class("end-box")

        highlight()
        t.observe(highlight, names="value")
        t.on_submit(self.run)
        return t

    def make_row(self, *vals):
        vals = list(vals) + [""] * (5 - len(vals))
        cells = [self.cell(v) for v in vals]

        delete_btn = widgets.Button(description="✖", layout=widgets.Layout(width=DEL_W))

        row = Row(cells, delete_btn, None)

        def kill(_):
            self.rows = [r for r in self.rows if r != row]
            if not self.rows:
                self.rows = [self.make_row()]
            self.refresh()

        delete_btn.on_click(kill)

        row.widget = widgets.HBox(cells + [delete_btn], layout=widgets.Layout(width=GRID_W))
        return row

    def refresh(self):
        self.grid.children = [r.widget for r in self.rows]

    def reset(self):
        self.rows = [self.make_row()]
        self.strings.value = ""
        self.refresh()
        self.out.clear_output()

    def add_row(self, _=None):
        self.rows.append(self.make_row())
        self.refresh()

    def load_example(self, _=None):
        self.strings.value = "xyz='111##'"
        data = [
            ("top", "cases R1", "move_back", "one_found", "hash_found"),
            ("one_found", "111#", "", "", ""),
            ("", "goto move_phase", "", "", ""),
            ("hash_found", "xyz", "", "", ""),
            ("", "goto move_phase", "", "", ""),
            ("move_phase", "move(2,3)+move(3,2)", "", "", ""),
            ("", "goto top", "", "", ""),
            ("move_back", "move(2,1)", "", "", ""),
        ]
        self.rows = [self.make_row(*d) for d in data]
        self.refresh()

    # ---------- validation ----------

    def clear_highlights(self):
        for r in self.rows:
            for b in r.boxes:
                b.remove_class("error-box")
                if b.value.strip() == "end":
                    b.add_class("end-box")
                else:
                    b.remove_class("end-box")

    def mark_error(self, box):
        box.add_class("error-box")

    def validate(self):
        defs = parse_defs(self.strings.value)
        labels = {}
        refs = []
        issues = []

        self.clear_highlights()

        for i, r in enumerate(self.rows, 1):
            if r.empty():
                continue

            b = r.vals()

            # CASES
            if is_cases(b[1]):
                if not is_label(b[0]):
                    issues.append(f"Row {i}: Box 1 can only contain labels")
                    self.mark_error(r.boxes[0])

                if not is_cases(b[1]):
                    issues.append(f"Row {i}: Box 2 must be 'cases k' or 'cases Rk'")
                    self.mark_error(r.boxes[1])

                for j, t in enumerate(b[2:5]):
                    if not (is_label(t) or t == "end"):
                        issues.append(f"Row {i}: Box {j+3} must be label or 'end'")
                        self.mark_error(r.boxes[j+2])
                    else:
                        refs.append(t)

            # GOTO
            elif b[1].startswith("goto"):
                parts = b[1].split()

                if len(parts) != 2:
                    if b[1] == "goto" and is_label(b[2]):
                        issues.append(f"Row {i}: Move label from Box 3 into Box 2")
                        self.mark_error(r.boxes[1])
                        self.mark_error(r.boxes[2])
                    else:
                        issues.append(f"Row {i}: Box 2 must be 'goto label'")
                        self.mark_error(r.boxes[1])

                else:
                    target = parts[1]
                    if not (is_label(target) or target == "end"):
                        issues.append(f"Row {i}: invalid goto target '{target}'")
                        self.mark_error(r.boxes[1])
                    else:
                        refs.append(target)

            # RAW
            else:
                if not is_label(b[0]):
                    issues.append(f"Row {i}: Box 1 can only contain labels")
                    self.mark_error(r.boxes[0])

                ok, msg = valid_expr(b[1], defs)
                if not ok:
                    issues.append(f"Row {i}: {msg}")
                    self.mark_error(r.boxes[1])

            # duplicate labels
            if is_label(b[0]):
                if b[0] in labels:
                    issues.append(f"Row {i}: duplicate label '{b[0]}'")
                    self.mark_error(r.boxes[0])
                labels[b[0]] = i

        # missing labels
        for t in refs:
            if t != "end" and t not in labels:
                issues.append(f"Missing label: {t}")

        return issues

    # ---------- execution ----------

    def eval_code(self, expr, defs):

        if re.fullmatch(r"[1#]+", expr):
            return expr

        if expr in defs:
            return "".join(defs[expr].split())

        val = str(
            eval(
                expr,
                {"__builtins__": {}, "move": move, "copy": copy} | defs,
                {}
            )
        )

        # remove all whitespace from generated 1# code
        return "".join(val.split())

    def plan(self):
        defs = parse_defs(self.strings.value)
        p = []
        counter = 0

        def fix(x):
            return "end" if x == "end" else x

        for r in self.rows:
            if r.empty():
                continue

            b = r.vals()
            label = b[0] if is_label(b[0]) else f"_L{counter}"
            counter += 1

            if is_cases(b[1]):
                p.append([label, "cases", cases_num(b[1]), fix(b[2]), fix(b[3]), fix(b[4])])

            elif b[1].startswith("goto"):
                parts = b[1].split()
                target = parts[1] if len(parts) > 1 else ""
                p.append([label, "goto", fix(target)])

            else:
                p.append([label, self.eval_code(b[1], defs)])

        return p

    def run(self, _=None):
        self.out.clear_output()

        try:
            issues = self.validate()
        except Exception as e:
            with self.out:
                print("Validation error:", e)
            return

        with self.out:
            if issues:
                print("Validation issues:")
                for e in issues:
                    print("-", e)
                return

            try:
                p = self.plan()

                print("PLAN:\n")
                for row in p:
                    print(row)

                print("\n1# program:\n")
                print(sanity(p))

            except Exception as e:
                print("sanity() error:", e)

    # ---------- UI ----------

    def build(self):
        headers = widgets.HBox([
            widgets.HTML("<b>Label</b>", layout=widgets.Layout(width=CELL_W)),
            widgets.HTML("<b>Instruction</b>", layout=widgets.Layout(width=CELL_W)),
            widgets.HTML("<b>'empty'</b>", layout=widgets.Layout(width=CELL_W)),
            widgets.HTML("<b>'1'</b>", layout=widgets.Layout(width=CELL_W)),
            widgets.HTML("<b>'#'</b>", layout=widgets.Layout(width=CELL_W)),
        ], layout=widgets.Layout(width=GRID_W))

        return widgets.VBox([
            widgets.HTML("""
            <style>
              .widget-textarea textarea {
                  resize: none !important;
              }
              .end-box input {
                  background-color: #e6ffe6 !important;
              }
              .error-box input {
                  background-color: #ffe6e6 !important;
              }
            </style>
            """),
            widgets.HTML("<b>1# idea tool</b>"),
            widgets.HTML("<b>String definitions</b>"),
            self.strings,
            headers,
            self.grid,
            widgets.HBox([self.add, self.load, self.clear, self.validate_btn], layout=widgets.Layout(width=GRID_W)),
            self.out
        ])

    def show(self):
        self.reset()
        display(self.ui)

app = IdeaTool()
app.show()
# 1# idea tool (Colab)
#
# Requires:
# !python -m pip install -U git+https://github.com/lmoss/onesharp.git@main
# from onesharp.interpreter.interpreter import *

import ast
import re
from dataclasses import dataclass
from typing import List

import ipywidgets as widgets
from IPython.display import display

RESERVED = {"cases", "goto", "end"}

CELL_W = "94px"
DEL_W = "32px"
GRID_W = "520px"

# ---------- helpers ----------

def is_label(s):
    return bool(re.fullmatch(r"[A-Za-z][A-Za-z0-9_]*", s or "")) and s not in RESERVED

def is_cases(s):
    return bool(re.fullmatch(r"cases\s+(?:R)?\d+", s or "", re.I))

def cases_num(s):
    m = re.fullmatch(r"cases\s+(?:R)?(\d+)", s or "", re.I)
    return int(m.group(1)) if m else None

def parse_defs(txt):
    d = {}
    for line in (txt or "").splitlines():
        if "=" in line:
            k, v = line.split("=", 1)
            k = k.strip()
            v = v.strip()
            try:
                val = ast.literal_eval(v)
                if isinstance(val, str):
                    d[k] = val
            except:
                pass
    return d

def valid_expr(expr, defs):
    if re.fullmatch(r"[1#]+", expr or ""):
        return True, ""
    if expr in defs:
        return True, ""
    try:
        tree = ast.parse(expr, mode="eval")
    except:
        return False, "invalid syntax"

    allowed = set(defs) | {"move", "copy"}

    for n in ast.walk(tree):
        if isinstance(n, ast.Name) and n.id not in allowed:
            return False, f"variable '{n.id}' is not defined"

    return True, ""

# ---------- row ----------

@dataclass
class Row:
    boxes: list
    delete: widgets.Button
    widget: widgets.HBox

    def vals(self):
        return [b.value.strip() for b in self.boxes]

    def empty(self):
        return not any(self.vals())

# ---------- tool ----------

class IdeaTool:

    def __init__(self):
        self.add = widgets.Button(description="Add segment")
        self.load = widgets.Button(description="Load example")
        self.clear = widgets.Button(description="Clear input")
        self.validate_btn = widgets.Button(description="Validate", button_style="primary")

        self.strings = widgets.Textarea(
            layout=widgets.Layout(width=GRID_W, height="80px")
        )

        self.rows = []
        self.grid = widgets.VBox(layout=widgets.Layout(gap="0px"))
        self.out = widgets.Output()

        self.add.on_click(self.add_row)
        self.load.on_click(self.load_example)
        self.clear.on_click(lambda _: self.reset())
        self.validate_btn.on_click(self.run)

        self.reset()
        self.ui = self.build()

    # ---------- UI helpers ----------

    def cell(self, v=""):
        t = widgets.Text(
            value=v,
            layout=widgets.Layout(width=CELL_W, height="26px")
        )

        def highlight(change=None):
            val = t.value.strip()
            if val == "end":
                t.add_class("end-box")
            else:
                t.remove_class("end-box")

        highlight()
        t.observe(highlight, names="value")
        t.on_submit(self.run)
        return t

    def make_row(self, *vals):
        vals = list(vals) + [""] * (5 - len(vals))
        cells = [self.cell(v) for v in vals]


        delete_btn = widgets.Button(
          description="Del",
          layout=widgets.Layout(width="42px")
          row = Row(cells, delete_btn, None))

        def kill(_):
            self.rows = [r for r in self.rows if r != row]
            if not self.rows:
                self.rows = [self.make_row()]
            self.refresh()

        delete_btn.on_click(kill)

        row.widget = widgets.HBox(cells + [delete_btn], layout=widgets.Layout(width=GRID_W))
        return row

    def refresh(self):
        self.grid.children = [r.widget for r in self.rows]

    def reset(self):
        self.rows = [self.make_row()]
        self.strings.value = ""
        self.refresh()
        self.out.clear_output()

    def add_row(self, _=None):
        self.rows.append(self.make_row())
        self.refresh()

    def load_example(self, _=None):
        self.strings.value = "xyz='111##'"
        data = [
            ("top", "cases R1", "move_back", "one_found", "hash_found"),
            ("one_found", "111#", "", "", ""),
            ("", "goto move_phase", "", "", ""),
            ("hash_found", "xyz", "", "", ""),
            ("", "goto move_phase", "", "", ""),
            ("move_phase", "move(2,3)+move(3,2)", "", "", ""),
            ("", "goto top", "", "", ""),
            ("move_back", "move(2,1)", "", "", ""),
        ]
        self.rows = [self.make_row(*d) for d in data]
        self.refresh()

    # ---------- validation ----------

    def clear_highlights(self):
        for r in self.rows:
            for b in r.boxes:
                b.remove_class("error-box")
                if b.value.strip() == "end":
                    b.add_class("end-box")
                else:
                    b.remove_class("end-box")

    def mark_error(self, box):
        box.add_class("error-box")

    def validate(self):
        defs = parse_defs(self.strings.value)
        labels = {}
        refs = []
        issues = []

        self.clear_highlights()

        for i, r in enumerate(self.rows, 1):
            if r.empty():
                continue

            b = r.vals()

            # CASES
            if is_cases(b[1]):
                if not is_label(b[0]):
                    issues.append(f"Row {i}: Box 1 can only contain labels")
                    self.mark_error(r.boxes[0])

                if not is_cases(b[1]):
                    issues.append(f"Row {i}: Box 2 must be 'cases k' or 'cases Rk'")
                    self.mark_error(r.boxes[1])

                for j, t in enumerate(b[2:5]):
                    if not (is_label(t) or t == "end"):
                        issues.append(f"Row {i}: Box {j+3} must be label or 'end'")
                        self.mark_error(r.boxes[j+2])
                    else:
                        refs.append(t)

            # GOTO
            elif b[1].startswith("goto"):
                parts = b[1].split()

                if len(parts) != 2:
                    if b[1] == "goto" and is_label(b[2]):
                        issues.append(f"Row {i}: Move label from Box 3 into Box 2")
                        self.mark_error(r.boxes[1])
                        self.mark_error(r.boxes[2])
                    else:
                        issues.append(f"Row {i}: Box 2 must be 'goto label'")
                        self.mark_error(r.boxes[1])

                else:
                    target = parts[1]
                    if not (is_label(target) or target == "end"):
                        issues.append(f"Row {i}: invalid goto target '{target}'")
                        self.mark_error(r.boxes[1])
                    else:
                        refs.append(target)

            # RAW
            else:
                if not is_label(b[0]):
                    issues.append(f"Row {i}: Box 1 can only contain labels")
                    self.mark_error(r.boxes[0])

                ok, msg = valid_expr(b[1], defs)
                if not ok:
                    issues.append(f"Row {i}: {msg}")
                    self.mark_error(r.boxes[1])

            # duplicate labels
            if is_label(b[0]):
                if b[0] in labels:
                    issues.append(f"Row {i}: duplicate label '{b[0]}'")
                    self.mark_error(r.boxes[0])
                labels[b[0]] = i

        # missing labels
        for t in refs:
            if t != "end" and t not in labels:
                issues.append(f"Missing label: {t}")

        return issues

    # ---------- execution ----------

    def eval_code(self, expr, defs):
        if re.fullmatch(r"[1#]+", expr):
            return expr
        if expr in defs:
            return defs[expr]
        return str(eval(expr, {"__builtins__": {}, "move": move, "copy": copy} | defs, {}))

    def plan(self):
        defs = parse_defs(self.strings.value)
        p = []
        counter = 0

        def fix(x):
            return "end" if x == "end" else x

        for r in self.rows:
            if r.empty():
                continue

            b = r.vals()
            label = b[0] if is_label(b[0]) else f"_L{counter}"
            counter += 1

            if is_cases(b[1]):
                p.append([label, "cases", cases_num(b[1]), fix(b[2]), fix(b[3]), fix(b[4])])

            elif b[1].startswith("goto"):
                parts = b[1].split()
                target = parts[1] if len(parts) > 1 else ""
                p.append([label, "goto", fix(target)])

            else:
                p.append([label, self.eval_code(b[1], defs)])

        return p

    def run(self, _=None):
        self.out.clear_output()

        try:
            issues = self.validate()
        except Exception as e:
            with self.out:
                print("Validation error:", e)
            return

        with self.out:
            if issues:
                print("Validation issues:")
                for e in issues:
                    print("-", e)
                return

            try:
                p = self.plan()

                print("PLAN:\n")
                for row in p:
                    print(row)

                print("\n1# program:\n")
                print(sanity(p))

            except Exception as e:
                print("sanity() error:", e)

    # ---------- UI ----------

    def build(self):
        headers = widgets.HBox([
            widgets.HTML("<b>Label</b>", layout=widgets.Layout(width=CELL_W)),
            widgets.HTML("<b>Instruction</b>", layout=widgets.Layout(width=CELL_W)),
            widgets.HTML("<b>'empty'</b>", layout=widgets.Layout(width=CELL_W)),
            widgets.HTML("<b>'1'</b>", layout=widgets.Layout(width=CELL_W)),
            widgets.HTML("<b>'#'</b>", layout=widgets.Layout(width=CELL_W)),
        ], layout=widgets.Layout(width=GRID_W))

        return widgets.VBox([
            widgets.HTML("""
            <style>
              .widget-textarea textarea {
                  resize: none !important;
              }
              .end-box input {
                  background-color: #e6ffe6 !important;
              }
              .error-box input {
                  background-color: #ffe6e6 !important;
              }
            </style>
            """),
            widgets.HTML("<b>1# idea tool</b>"),
            widgets.HTML("<b>String definitions</b>"),
            self.strings,
            headers,
            self.grid,
            widgets.HBox([self.add, self.load, self.clear, self.validate_btn], layout=widgets.Layout(width=GRID_W)),
            self.out
        ])

    def show(self):
        self.reset()
        display(self.ui)

app = IdeaTool()
app.show()
  File "/tmp/ipykernel_13474/631824215.py", line 133
    layout=widgets.Layout(width="42px")
           ^
SyntaxError: invalid syntax. Perhaps you forgot a comma?
def eval_code(self, expr, defs):

    if re.fullmatch(r"[1#]+", expr):
        return expr

    if expr in defs:
        return "".join(defs[expr].split())

    val = str(
        eval(
            expr,
            {"__builtins__": {}, "move": move, "copy": copy} | defs,
            {}
        )
    )

    # remove all whitespace from generated 1# code
    return "".join(val.split())
print(self.eval_code("move(2,3)+move(3,2)", {}))
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
/tmp/ipykernel_13474/2242439539.py in <cell line: 0>()
----> 1 print(self.eval_code("move(2,3)+move(3,2)", {}))

AttributeError: 'str' object has no attribute 'eval_code'
app = IdeaTool()
app.show()
# 1# idea tool (Colab)
#
# Requires:
# !python -m pip install -U git+https://github.com/lmoss/onesharp.git@main
# from onesharp.interpreter.interpreter import *

import ast
import re
from dataclasses import dataclass
from typing import List

import ipywidgets as widgets
from IPython.display import display

RESERVED = {"cases", "goto", "end"}

CELL_W = "94px"
DEL_W = "48px"
GRID_W = "540px"

# ---------- helpers ----------

def is_label(s):
    return bool(re.fullmatch(r"[A-Za-z][A-Za-z0-9_]*", s or "")) and s not in RESERVED

def is_cases(s):
    return bool(re.fullmatch(r"cases\s+(?:R)?\d+", s or "", re.I))

def cases_num(s):
    m = re.fullmatch(r"cases\s+(?:R)?(\d+)", s or "", re.I)
    return int(m.group(1)) if m else None

def parse_defs(txt):
    d = {}
    for line in (txt or "").splitlines():
        if "=" in line:
            k, v = line.split("=", 1)
            k = k.strip()
            v = v.strip()
            try:
                val = ast.literal_eval(v)
                if isinstance(val, str):
                    d[k] = val
            except:
                pass
    return d

def valid_expr(expr, defs):
    if re.fullmatch(r"[1#]+", expr or ""):
        return True, ""
    if expr in defs:
        return True, ""
    try:
        tree = ast.parse(expr, mode="eval")
    except:
        return False, "invalid syntax"

    allowed = set(defs) | {"move", "copy"}

    for n in ast.walk(tree):
        if isinstance(n, ast.Name) and n.id not in allowed:
            return False, f"variable '{n.id}' is not defined"

    return True, ""

# ---------- row ----------

@dataclass
class Row:
    boxes: list
    delete: widgets.Button
    widget: widgets.HBox

    def vals(self):
        return [b.value.strip() for b in self.boxes]

    def empty(self):
        return not any(self.vals())

# ---------- tool ----------

class IdeaTool:

    def __init__(self):
        self.add = widgets.Button(description="Add segment")
        self.load = widgets.Button(description="Load example")
        self.clear = widgets.Button(description="Clear input")
        self.validate_btn = widgets.Button(description="Validate", button_style="primary")

        self.strings = widgets.Textarea(
            layout=widgets.Layout(width=GRID_W, height="80px")
        )

        self.rows = []
        self.grid = widgets.VBox(layout=widgets.Layout(gap="0px"))
        self.out = widgets.Output()

        self.add.on_click(self.add_row)
        self.load.on_click(self.load_example)
        self.clear.on_click(lambda _: self.reset())
        self.validate_btn.on_click(self.run)

        self.reset()
        self.ui = self.build()

    # ---------- UI helpers ----------

    def cell(self, v=""):
        t = widgets.Text(
            value=v,
            layout=widgets.Layout(width=CELL_W, height="26px")
        )

        def highlight(change=None):
            val = t.value.strip()
            if val == "end":
                t.add_class("end-box")
            else:
                t.remove_class("end-box")

        highlight()
        t.observe(highlight, names="value")
        t.on_submit(self.run)
        return t

    def make_row(self, *vals):
        vals = list(vals) + [""] * (5 - len(vals))
        cells = [self.cell(v) for v in vals]

        #delete_btn = widgets.Button(description="✖", layout=widgets.Layout(width=DEL_W))
        delete_btn = widgets.Button(
          description="Del",
          layout=widgets.Layout(width="42px"))
        row = Row(cells, delete_btn, None)

        def kill(_):
            self.rows = [r for r in self.rows if r != row]
            if not self.rows:
                self.rows = [self.make_row()]
            self.refresh()

        delete_btn.on_click(kill)

        row.widget = widgets.HBox(cells + [delete_btn], layout=widgets.Layout(width=GRID_W))
        return row

    def refresh(self):
        self.grid.children = [r.widget for r in self.rows]

    def reset(self):
        self.rows = [self.make_row()]
        self.strings.value = ""
        self.refresh()
        self.out.clear_output()

    def add_row(self, _=None):
        self.rows.append(self.make_row())
        self.refresh()

    def load_example(self, _=None):
        self.strings.value = "xyz='111##'"
        data = [
            ("top", "cases R1", "move_back", "one_found", "hash_found"),
            ("one_found", "111#", "", "", ""),
            ("", "goto move_phase", "", "", ""),
            ("hash_found", "xyz", "", "", ""),
            ("", "goto move_phase", "", "", ""),
            ("move_phase", "move(2,3)+move(3,2)", "", "", ""),
            ("", "goto top", "", "", ""),
            ("move_back", "move(2,1)", "", "", ""),
        ]
        self.rows = [self.make_row(*d) for d in data]
        self.refresh()

    # ---------- validation ----------

    def clear_highlights(self):
        for r in self.rows:
            for b in r.boxes:
                b.remove_class("error-box")
                if b.value.strip() == "end":
                    b.add_class("end-box")
                else:
                    b.remove_class("end-box")

    def mark_error(self, box):
        box.add_class("error-box")

    def validate(self):
        defs = parse_defs(self.strings.value)
        labels = {}
        refs = []
        issues = []

        self.clear_highlights()

        for i, r in enumerate(self.rows, 1):
            if r.empty():
                continue

            b = r.vals()

            # CASES
            if is_cases(b[1]):
                if not is_label(b[0]):
                    issues.append(f"Row {i}: Box 1 can only contain labels")
                    self.mark_error(r.boxes[0])

                if not is_cases(b[1]):
                    issues.append(f"Row {i}: Box 2 must be 'cases k' or 'cases Rk'")
                    self.mark_error(r.boxes[1])

                for j, t in enumerate(b[2:5]):
                    if not (is_label(t) or t == "end"):
                        issues.append(f"Row {i}: Box {j+3} must be label or 'end'")
                        self.mark_error(r.boxes[j+2])
                    else:
                        refs.append(t)

            # GOTO
            elif b[1].startswith("goto"):
                parts = b[1].split()

                if len(parts) != 2:
                    if b[1] == "goto" and is_label(b[2]):
                        issues.append(f"Row {i}: Move label from Box 3 into Box 2")
                        self.mark_error(r.boxes[1])
                        self.mark_error(r.boxes[2])
                    else:
                        issues.append(f"Row {i}: Box 2 must be 'goto label'")
                        self.mark_error(r.boxes[1])

                else:
                    target = parts[1]
                    if not (is_label(target) or target == "end"):
                        issues.append(f"Row {i}: invalid goto target '{target}'")
                        self.mark_error(r.boxes[1])
                    else:
                        refs.append(target)

            # RAW
            else:
                if not is_label(b[0]):
                    issues.append(f"Row {i}: Box 1 can only contain labels")
                    self.mark_error(r.boxes[0])

                ok, msg = valid_expr(b[1], defs)
                if not ok:
                    issues.append(f"Row {i}: {msg}")
                    self.mark_error(r.boxes[1])

            # duplicate labels
            if is_label(b[0]):
                if b[0] in labels:
                    issues.append(f"Row {i}: duplicate label '{b[0]}'")
                    self.mark_error(r.boxes[0])
                labels[b[0]] = i

        # missing labels
        for t in refs:
            if t != "end" and t not in labels:
                issues.append(f"Missing label: {t}")

        return issues

    # ---------- execution ----------

    def eval_code(self, expr, defs):
        if re.fullmatch(r"[1#]+", expr):
            return expr
        if expr in defs:
            return defs[expr]
        return str(eval(expr, {"__builtins__": {}, "move": move, "copy": copy} | defs, {}))

    def plan(self):
        defs = parse_defs(self.strings.value)
        p = []
        counter = 0

        def fix(x):
            return "end" if x == "end" else x

        for r in self.rows:
            if r.empty():
                continue

            b = r.vals()
            label = b[0] if is_label(b[0]) else f"_L{counter}"
            counter += 1

            if is_cases(b[1]):
                p.append([label, "cases", cases_num(b[1]), fix(b[2]), fix(b[3]), fix(b[4])])

            elif b[1].startswith("goto"):
                parts = b[1].split()
                target = parts[1] if len(parts) > 1 else ""
                p.append([label, "goto", fix(target)])

            else:
                p.append([label, self.eval_code(b[1], defs)])

        return p

    def run(self, _=None):
        self.out.clear_output()

        try:
            issues = self.validate()
        except Exception as e:
            with self.out:
                print("Validation error:", e)
            return

        with self.out:
            if issues:
                print("Validation issues:")
                for e in issues:
                    print("-", e)
                return

            try:
                p = self.plan()

                print("PLAN:\n")
                for row in p:
                    print(row)

                print("\n1# program:\n")
                print(sanity(p))

            except Exception as e:
                print("sanity() error:", e)

    # ---------- UI ----------

    def build(self):
        headers = widgets.HBox([
            widgets.HTML("<b>Label</b>", layout=widgets.Layout(width=CELL_W)),
            widgets.HTML("<b>Instruction</b>", layout=widgets.Layout(width=CELL_W)),
            widgets.HTML("<b>'empty'</b>", layout=widgets.Layout(width=CELL_W)),
            widgets.HTML("<b>'1'</b>", layout=widgets.Layout(width=CELL_W)),
            widgets.HTML("<b>'#'</b>", layout=widgets.Layout(width=CELL_W)),
        ], layout=widgets.Layout(width=GRID_W))

        return widgets.VBox([
            widgets.HTML("""
            <style>
              .widget-textarea textarea {
                  resize: none !important;
              }
              .end-box input {
                  background-color: #e6ffe6 !important;
              }
              .error-box input {
                  background-color: #ffe6e6 !important;
              }
            </style>
            """),
            widgets.HTML("<b>1# idea tool</b>"),
            widgets.HTML("<b>String definitions</b>"),
            self.strings,
            headers,
            self.grid,
            widgets.HBox([self.add, self.load, self.clear, self.validate_btn], layout=widgets.Layout(width=GRID_W)),
            self.out
        ])

    def show(self):
        self.reset()
        display(self.ui)

app = IdeaTool()
app.show()
app.show()
u
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
/tmp/ipykernel_5308/1136808918.py in <cell line: 0>()
----> 1 u

NameError: name 'u' is not defined
diag
'1#####11111111111###111111###11##111#111##111##1111111####11#111#111##1111####111#####111111###111###1##1111####1#11####11#####111111###111###1##1111####1#11####'