about summary refs log tree commit diff stats
diff options
context:
space:
mode:
authorKartik K. Agaram <vc@akkartik.com>2023-11-18 11:30:57 -0800
committerKartik K. Agaram <vc@akkartik.com>2023-11-18 11:32:01 -0800
commit007b965b11b681550ee2e2244a2f53e64e88697d (patch)
treee3bff0e0d71e896ea1d954ae7715f672b247bf0e
parent5cce5115507800eeca7ba9c271e07c23473228f4 (diff)
downloadlines.love-007b965b11b681550ee2e2244a2f53e64e88697d.tar.gz
audit all asserts
Each one should provide a message that will show up within LÖVE. Stop
relying on nearby prints to the terminal.

I also found some unnecessary ones.

There is some potential here for performance regressions: the format()
calls will trigger whether or not the assertion fails, and cause
allocations. So far Lua's GC seems good enough to manage the load even
with Moby Dick, even in some situations that caused issues in the past
like undo.
-rw-r--r--commands.lua2
-rw-r--r--drawing.lua28
-rw-r--r--edit.lua15
-rw-r--r--file.lua13
-rw-r--r--geom.lua3
-rw-r--r--log_browser.lua4
-rw-r--r--search.lua2
-rw-r--r--select.lua10
-rw-r--r--source_edit.lua13
-rw-r--r--source_file.lua13
-rw-r--r--source_select.lua10
-rw-r--r--source_text.lua45
-rw-r--r--source_undo.lua15
-rw-r--r--text.lua45
-rw-r--r--undo.lua14
15 files changed, 100 insertions, 132 deletions
diff --git a/commands.lua b/commands.lua
index a76bae5..1f8e304 100644
--- a/commands.lua
+++ b/commands.lua
@@ -136,7 +136,7 @@ end
 
 function move_candidate_to_front(s)
   local index = array.find(File_navigation.all_candidates, s)
-  assert(index)
+  assert(index, 'file missing from manifest')
   table.remove(File_navigation.all_candidates, index)
   table.insert(File_navigation.all_candidates, 1, s)
 end
diff --git a/drawing.lua b/drawing.lua
index a98f5b2..615ea62 100644
--- a/drawing.lua
+++ b/drawing.lua
@@ -33,7 +33,6 @@ function Drawing.draw(State, line_index, y)
   local my = Drawing.coord(pmy-line_cache.starty, State.width)
 
   for _,shape in ipairs(line.shapes) do
-    assert(shape)
     if geom.on_shape(mx,my, line, shape) then
       App.color(Focus_stroke_color)
     else
@@ -113,8 +112,7 @@ function Drawing.draw_shape(drawing, shape, top, left,right)
   elseif shape.mode == 'deleted' then
     -- ignore
   else
-    print(shape.mode)
-    assert(false)
+    assert(false, ('unknown drawing mode %s'):format(shape.mode))
   end
 end
 
@@ -207,8 +205,7 @@ function Drawing.draw_pending_shape(drawing, top, left,right)
   elseif shape.mode == 'name' then
     -- nothing pending; changes are immediately committed
   else
-    print(shape.mode)
-    assert(false)
+    assert(false, ('unknown drawing mode %s'):format(shape.mode))
   end
 end
 
@@ -239,8 +236,7 @@ function Drawing.mouse_press(State, drawing_index, x,y, mouse_button)
   elseif State.current_drawing_mode == 'name' then
     -- nothing
   else
-    print(State.current_drawing_mode)
-    assert(false)
+    assert(false, ('unknown drawing mode %s'):format(State.current_drawing_mode))
   end
 end
 
@@ -255,7 +251,7 @@ function Drawing.update(State)
     -- just skip this frame
     return
   end
-  assert(drawing.mode == 'drawing')
+  assert(drawing.mode == 'drawing', 'Drawing.update: line is not a drawing')
   local pmx, pmy = App.mouse_x(), App.mouse_y()
   local mx = Drawing.coord(pmx-State.left, State.width)
   local my = Drawing.coord(pmy-line_cache.starty, State.width)
@@ -342,7 +338,7 @@ function Drawing.mouse_release(State, x,y, mouse_button)
           table.insert(drawing.shapes, drawing.pending)
         end
       elseif drawing.pending.mode == 'rectangle' then
-        assert(#drawing.pending.vertices <= 2)
+        assert(#drawing.pending.vertices <= 2, 'Drawing.mouse_release: rectangle has too many pending vertices')
         if #drawing.pending.vertices == 2 then
           local mx,my = Drawing.coord(x-State.left, State.width), Drawing.coord(y-line_cache.starty, State.width)
           if mx >= 0 and mx < 256 and my >= 0 and my < drawing.h then
@@ -357,7 +353,7 @@ function Drawing.mouse_release(State, x,y, mouse_button)
           -- too few points; draw nothing
         end
       elseif drawing.pending.mode == 'square' then
-        assert(#drawing.pending.vertices <= 2)
+        assert(#drawing.pending.vertices <= 2, 'Drawing.mouse_release: square has too many pending vertices')
         if #drawing.pending.vertices == 2 then
           local mx,my = Drawing.coord(x-State.left, State.width), Drawing.coord(y-line_cache.starty, State.width)
           if mx >= 0 and mx < 256 and my >= 0 and my < drawing.h then
@@ -386,8 +382,7 @@ function Drawing.mouse_release(State, x,y, mouse_button)
       elseif drawing.pending.mode == 'name' then
         -- drop it
       else
-        print(drawing.pending.mode)
-        assert(false)
+        assert(false, ('unknown drawing mode %s'):format(drawing.pending.mode))
       end
       State.lines.current_drawing.pending = {}
       State.lines.current_drawing = nil
@@ -545,7 +540,7 @@ function Drawing.keychord_press(State, chord)
         if Drawing.contains_point(shape, i) then
           if shape.mode == 'polygon' then
             local idx = table.find(shape.vertices, i)
-            assert(idx)
+            assert(idx, 'point to delete is not in vertices')
             table.remove(shape.vertices, idx)
             if #shape.vertices < 3 then
               shape.mode = 'deleted'
@@ -641,7 +636,6 @@ function Drawing.select_shape_at_mouse(State)
       if Drawing.in_drawing(drawing, line_cache, x,y, State.left,State.right) then
         local mx,my = Drawing.coord(x-State.left, State.width), Drawing.coord(y-line_cache.starty, State.width)
         for i,shape in ipairs(drawing.shapes) do
-          assert(shape)
           if geom.on_shape(mx,my, drawing, shape) then
             return drawing,line_cache,i,shape
           end
@@ -659,7 +653,6 @@ function Drawing.select_point_at_mouse(State)
       if Drawing.in_drawing(drawing, line_cache, x,y, State.left,State.right) then
         local mx,my = Drawing.coord(x-State.left, State.width), Drawing.coord(y-line_cache.starty, State.width)
         for i,point in ipairs(drawing.points) do
-          assert(point)
           if Drawing.near(point, mx,my, State.width) then
             return drawing_index,drawing,line_cache,i,point
           end
@@ -696,13 +689,12 @@ function Drawing.contains_point(shape, p)
   elseif shape.mode == 'deleted' then
     -- already done
   else
-    print(shape.mode)
-    assert(false)
+    assert(false, ('unknown drawing mode %s'):format(shape.mode))
   end
 end
 
 function Drawing.smoothen(shape)
-  assert(shape.mode == 'freehand')
+  assert(shape.mode == 'freehand', 'can only smoothen freehand shapes')
   for _=1,7 do
     for i=2,#shape.points-1 do
       local a = shape.points[i-1]
diff --git a/edit.lua b/edit.lua
index 232d9b9..c8d5a03 100644
--- a/edit.lua
+++ b/edit.lua
@@ -80,7 +80,7 @@ function edit.initialize_state(top, left, right, font_height, line_height)  -- c
     cursor_x = 0,
     cursor_y = 0,
 
-    current_drawing_mode = 'line',
+    current_drawing_mode = 'line',  -- one of the available shape modes
     previous_drawing_mode = nil,  -- extra state for some ephemeral modes like moving/deleting/naming points
 
     font_height = font_height,
@@ -157,14 +157,8 @@ end
 function edit.draw(State)
   State.button_handlers = {}
   App.color(Text_color)
-  if #State.lines ~= #State.line_cache then
-    print(('line_cache is out of date; %d when it should be %d'):format(#State.line_cache, #State.lines))
-    assert(false)
-  end
-  if not Text.le1(State.screen_top1, State.cursor1) then
-    print(State.screen_top1.line, State.screen_top1.pos, State.cursor1.line, State.cursor1.pos)
-    assert(false)
-  end
+  assert(#State.lines == #State.line_cache, ('line_cache is out of date; %d elements when it should be %d'):format(#State.line_cache, #State.lines))
+  assert(Text.le1(State.screen_top1, State.cursor1), ('screen_top (line=%d,pos=%d) is below cursor (line=%d,pos=%d)'):format(State.screen_top1.line, State.screen_top1.pos, State.cursor1.line, State.cursor1.pos))
   State.cursor_x = nil
   State.cursor_y = nil
   local y = State.top
@@ -204,8 +198,7 @@ function edit.draw(State)
       Drawing.draw(State, line_index, y)
       y = y + Drawing.pixels(line.h, State.width) + Drawing_padding_bottom
     else
-      print(line.mode)
-      assert(false)
+      assert(false, ('unknown line mode %s'):format(line.mode))
     end
   end
   State.screen_bottom1 = screen_bottom1
diff --git a/file.lua b/file.lua
index 228f1f3..34be015 100644
--- a/file.lua
+++ b/file.lua
@@ -55,7 +55,7 @@ function load_drawing(infile_next_line)
   local drawing = {mode='drawing', h=256/2, points={}, shapes={}, pending={}}
   while true do
     local line = infile_next_line()
-    assert(line)
+    assert(line, 'drawing in file is incomplete')
     if line == '```' then break end
     local shape = json.decode(line)
     if shape.mode == 'freehand' then
@@ -80,8 +80,7 @@ function load_drawing(infile_next_line)
     elseif shape.mode == 'deleted' then
       -- ignore
     else
-      print(shape.mode)
-      assert(false)
+      assert(false, ('unknown drawing mode %s'):format(shape.mode))
     end
     table.insert(drawing.shapes, shape)
   end
@@ -115,8 +114,7 @@ function store_drawing(outfile, drawing)
     elseif shape.mode == 'deleted' then
       -- ignore
     else
-      print(shape.mode)
-      assert(false)
+      assert(false, ('unknown drawing mode %s'):format(shape.mode))
     end
   end
   outfile:write('```\n')
@@ -152,7 +150,7 @@ function load_drawing_from_array(iter, a, i)
   local line
   while true do
     i, line = iter(a, i)
-    assert(i)
+    assert(i, 'drawing in array is incomplete')
 --?     print(i)
     if line == '```' then break end
     local shape = json.decode(line)
@@ -178,8 +176,7 @@ function load_drawing_from_array(iter, a, i)
     elseif shape.mode == 'deleted' then
       -- ignore
     else
-      print(shape.mode)
-      assert(false)
+      assert(false, ('unknown drawing mode %s'):format(shape.mode))
     end
     table.insert(drawing.shapes, shape)
   end
diff --git a/geom.lua b/geom.lua
index 891e98d..ea007b7 100644
--- a/geom.lua
+++ b/geom.lua
@@ -38,8 +38,7 @@ function geom.on_shape(x,y, drawing, shape)
     return geom.angle_between(center.x,center.y, x,y, shape.start_angle,shape.end_angle)
   elseif shape.mode == 'deleted' then
   else
-    print(shape.mode)
-    assert(false)
+    assert(false, ('unknown drawing mode %s'):format(shape.mode))
   end
 end
 
diff --git a/log_browser.lua b/log_browser.lua
index 6b71da6..b6195b9 100644
--- a/log_browser.lua
+++ b/log_browser.lua
@@ -75,7 +75,7 @@ function table.shallowcopy(x)
 end
 
 function log_browser.draw(State, hide_cursor)
-  assert(#State.lines == #State.line_cache)
+  assert(#State.lines == #State.line_cache, ('line_cache is out of date; %d elements when it should be %d'):format(#State.line_cache, #State.lines))
   local mouse_line_index = log_browser.line_index(State, App.mouse_x(), App.mouse_y())
   local y = State.top
   for line_index = State.screen_top1.line,#State.lines do
@@ -95,7 +95,7 @@ function log_browser.draw(State, hide_cursor)
           love.graphics.line(xleft,sectiony, xleft+50-2,sectiony)
           love.graphics.print(line.section_name, xleft+50,y)
           love.graphics.line(xleft+50+App.width(line.section_name)+2,sectiony, xright,sectiony)
-        else assert(line.section_end)
+        else assert(line.section_end, "log line has a section name, but it's neither the start nor end of a section")
           local sectiony = y+State.line_height-Section_border_padding_vertical
           love.graphics.line(xleft,y, xleft,sectiony)
           love.graphics.line(xright,y, xright,sectiony)
diff --git a/search.lua b/search.lua
index fe57ac9..67b82b3 100644
--- a/search.lua
+++ b/search.lua
@@ -139,7 +139,7 @@ function rfind(s, pat, i, plain)
   local rendpos = rs:find(rpat, ri, plain)
   if rendpos == nil then return nil end
   local endpos = #s - rendpos + 1
-  assert (endpos >= #pat)
+  assert (endpos >= #pat, ('rfind: endpos %d should be >= #pat %d at this point'):format(endpos, #pat))
   return endpos-#pat+1
 end
 
diff --git a/select.lua b/select.lua
index 9b2a278..9ede1da 100644
--- a/select.lua
+++ b/select.lua
@@ -33,13 +33,13 @@ function Text.clip_selection(State, line_index, apos, bpos)
     -- fully contained
     return apos,bpos
   elseif a_ge then
-    assert(maxl == line_index)
+    assert(maxl == line_index, ('maxl %d not equal to line_index %d'):format(maxl, line_index))
     return apos,maxp
   elseif b_lt then
-    assert(minl == line_index)
+    assert(minl == line_index, ('minl %d not equal to line_index %d'):format(minl, line_index))
     return minp,bpos
   else
-    assert(minl == maxl and minl == line_index)
+    assert(minl == maxl and minl == line_index, ('minl %d, maxl %d and line_index %d are not all equal'):format(minl, maxl, line_index))
     return minp,maxp
   end
 end
@@ -127,7 +127,7 @@ function Text.delete_selection_without_undo(State)
     State.lines[minl].data = State.lines[minl].data:sub(1, min_offset-1)..State.lines[minl].data:sub(max_offset)
     return
   end
-  assert(minl < maxl)
+  assert(minl < maxl, ('minl %d not < maxl %d'):format(minl, maxl))
   local rhs = State.lines[maxl].data:sub(max_offset)
   for i=maxl,minl+1,-1 do
     table.remove(State.lines, i)
@@ -154,7 +154,7 @@ function Text.selection(State)
   if minl == maxl then
     return State.lines[minl].data:sub(min_offset, max_offset-1)
   end
-  assert(minl < maxl)
+  assert(minl < maxl, ('minl %d not < maxl %d'):format(minl, maxl))
   local result = {State.lines[minl].data:sub(min_offset)}
   for i=minl+1,maxl-1 do
     if State.lines[i].mode == 'text' then
diff --git a/source_edit.lua b/source_edit.lua
index 12c2bae..6259ebe 100644
--- a/source_edit.lua
+++ b/source_edit.lua
@@ -158,14 +158,8 @@ end
 function edit.draw(State, hide_cursor, show_line_numbers)
   State.button_handlers = {}
   App.color(Text_color)
-  if #State.lines ~= #State.line_cache then
-    print(('line_cache is out of date; %d when it should be %d'):format(#State.line_cache, #State.lines))
-    assert(false)
-  end
-  if not Text.le1(State.screen_top1, State.cursor1) then
-    print(State.screen_top1.line, State.screen_top1.pos, State.cursor1.line, State.cursor1.pos)
-    assert(false)
-  end
+  assert(#State.lines == #State.line_cache, ('line_cache is out of date; %d elements when it should be %d'):format(#State.line_cache, #State.lines))
+  assert(Text.le1(State.screen_top1, State.cursor1), ('screen_top (line=%d,pos=%d) is below cursor (line=%d,pos=%d)'):format(State.screen_top1.line, State.screen_top1.pos, State.cursor1.line, State.cursor1.pos))
   State.cursor_x = nil
   State.cursor_y = nil
   local y = State.top
@@ -209,8 +203,7 @@ function edit.draw(State, hide_cursor, show_line_numbers)
       Drawing.draw(State, line_index, y)
       y = y + Drawing.pixels(line.h, State.width) + Drawing_padding_bottom
     else
-      print(line.mode)
-      assert(false)
+      assert(false, ('unknown line mode %s'):format(line.mode))
     end
   end
   State.screen_bottom1 = screen_bottom1
diff --git a/source_file.lua b/source_file.lua
index dbb6f95..6d04f6b 100644
--- a/source_file.lua
+++ b/source_file.lua
@@ -63,7 +63,7 @@ function load_drawing(infile_next_line)
   local drawing = {mode='drawing', h=256/2, points={}, shapes={}, pending={}}
   while true do
     local line = infile_next_line()
-    assert(line)
+    assert(line, 'drawing in file is incomplete')
     if line == '```' then break end
     local shape = json.decode(line)
     if shape.mode == 'freehand' then
@@ -88,8 +88,7 @@ function load_drawing(infile_next_line)
     elseif shape.mode == 'deleted' then
       -- ignore
     else
-      print(shape.mode)
-      assert(false)
+      assert(false, ('unknown drawing mode %s'):format(shape.mode))
     end
     table.insert(drawing.shapes, shape)
   end
@@ -123,8 +122,7 @@ function store_drawing(outfile, drawing)
     elseif shape.mode == 'deleted' then
       -- ignore
     else
-      print(shape.mode)
-      assert(false)
+      assert(false, ('unknown drawing mode %s'):format(shape.mode))
     end
   end
   outfile:write('```\n')
@@ -162,7 +160,7 @@ function load_drawing_from_array(iter, a, i)
   local line
   while true do
     i, line = iter(a, i)
-    assert(i)
+    assert(i, 'drawing in array is incomplete')
 --?     print(i)
     if line == '```' then break end
     local shape = json.decode(line)
@@ -188,8 +186,7 @@ function load_drawing_from_array(iter, a, i)
     elseif shape.mode == 'deleted' then
       -- ignore
     else
-      print(shape.mode)
-      assert(false)
+      assert(false, ('unknown drawing mode %s'):format(shape.mode))
     end
     table.insert(drawing.shapes, shape)
   end
diff --git a/source_select.lua b/source_select.lua
index 9b2a278..9ede1da 100644
--- a/source_select.lua
+++ b/source_select.lua
@@ -33,13 +33,13 @@ function Text.clip_selection(State, line_index, apos, bpos)
     -- fully contained
     return apos,bpos
   elseif a_ge then
-    assert(maxl == line_index)
+    assert(maxl == line_index, ('maxl %d not equal to line_index %d'):format(maxl, line_index))
     return apos,maxp
   elseif b_lt then
-    assert(minl == line_index)
+    assert(minl == line_index, ('minl %d not equal to line_index %d'):format(minl, line_index))
     return minp,bpos
   else
-    assert(minl == maxl and minl == line_index)
+    assert(minl == maxl and minl == line_index, ('minl %d, maxl %d and line_index %d are not all equal'):format(minl, maxl, line_index))
     return minp,maxp
   end
 end
@@ -127,7 +127,7 @@ function Text.delete_selection_without_undo(State)
     State.lines[minl].data = State.lines[minl].data:sub(1, min_offset-1)..State.lines[minl].data:sub(max_offset)
     return
   end
-  assert(minl < maxl)
+  assert(minl < maxl, ('minl %d not < maxl %d'):format(minl, maxl))
   local rhs = State.lines[maxl].data:sub(max_offset)
   for i=maxl,minl+1,-1 do
     table.remove(State.lines, i)
@@ -154,7 +154,7 @@ function Text.selection(State)
   if minl == maxl then
     return State.lines[minl].data:sub(min_offset, max_offset-1)
   end
-  assert(minl < maxl)
+  assert(minl < maxl, ('minl %d not < maxl %d'):format(minl, maxl))
   local result = {State.lines[minl].data:sub(min_offset)}
   for i=minl+1,maxl-1 do
     if State.lines[i].mode == 'text' then
diff --git a/source_text.lua b/source_text.lua
index fb5123a..8b34d52 100644
--- a/source_text.lua
+++ b/source_text.lua
@@ -17,7 +17,7 @@ function Text.draw(State, line_index, y, startpos, hide_cursor, show_line_number
     love.graphics.print(line_index, State.left-Line_number_width*App.width('m')+10,y)
   end
   initialize_color()
-  assert(#line_cache.screen_line_starting_pos >= 1)
+  assert(#line_cache.screen_line_starting_pos >= 1, 'line cache missing screen line info')
   for i=1,#line_cache.screen_line_starting_pos do
     local pos = line_cache.screen_line_starting_pos[i]
     if pos < startpos then
@@ -209,7 +209,7 @@ function Text.text_input(State, t)
 end
 
 function Text.insert_at_cursor(State, t)
-  assert(State.lines[State.cursor1.line].mode == 'text')
+  assert(State.lines[State.cursor1.line].mode == 'text', 'line is not text')
   local byte_offset = Text.offset(State.lines[State.cursor1.line].data, State.cursor1.pos)
   State.lines[State.cursor1.line].data = string.sub(State.lines[State.cursor1.line].data, 1, byte_offset-1)..t..string.sub(State.lines[State.cursor1.line].data, byte_offset)
   Text.clear_screen_line_cache(State, State.cursor1.line)
@@ -286,7 +286,7 @@ function Text.keychord_press(State, chord)
       Text.redraw_all(State)  -- if we're scrolling, reclaim all fragments to avoid memory leaks
     end
     Text.clear_screen_line_cache(State, State.cursor1.line)
-    assert(Text.le1(State.screen_top1, State.cursor1))
+    assert(Text.le1(State.screen_top1, State.cursor1), ('screen_top (line=%d,pos=%d) is below cursor (line=%d,pos=%d)'):format(State.screen_top1.line, State.screen_top1.pos, State.cursor1.line, State.cursor1.pos))
     schedule_save(State)
     record_undo_event(State, {before=before, after=snapshot(State, State.cursor1.line)})
   elseif chord == 'delete' then
@@ -452,7 +452,7 @@ function Text.pagedown(State)
 end
 
 function Text.up(State)
-  assert(State.lines[State.cursor1.line].mode == 'text')
+  assert(State.lines[State.cursor1.line].mode == 'text', 'line is not text')
 --?   print('up', State.cursor1.line, State.cursor1.pos, State.screen_top1.line, State.screen_top1.pos)
   local screen_line_starting_pos, screen_line_index = Text.pos_at_start_of_screen_line(State, State.cursor1)
   if screen_line_starting_pos == 1 then
@@ -478,7 +478,7 @@ function Text.up(State)
     end
   else
     -- move up one screen line in current line
-    assert(screen_line_index > 1)
+    assert(screen_line_index > 1, 'bumped up against top screen line in line')
     local new_screen_line_starting_pos = State.line_cache[State.cursor1.line].screen_line_starting_pos[screen_line_index-1]
     local new_screen_line_starting_byte_offset = Text.offset(State.lines[State.cursor1.line].data, new_screen_line_starting_pos)
     local s = string.sub(State.lines[State.cursor1.line].data, new_screen_line_starting_byte_offset)
@@ -495,9 +495,9 @@ function Text.up(State)
 end
 
 function Text.down(State)
-  assert(State.lines[State.cursor1.line].mode == 'text')
+  assert(State.lines[State.cursor1.line].mode == 'text', 'line is not text')
 --?   print('down', State.cursor1.line, State.cursor1.pos, State.screen_top1.line, State.screen_top1.pos, State.screen_bottom1.line, State.screen_bottom1.pos)
-  assert(State.cursor1.pos)
+  assert(State.cursor1.pos, 'cursor has no pos')
   if Text.cursor_at_final_screen_line(State) then
     -- line is done, skip to next text line
 --?     print('cursor at final screen line of its line')
@@ -571,7 +571,7 @@ function Text.word_left(State)
     if State.cursor1.pos == 1 then
       break
     end
-    assert(State.cursor1.pos > 1)
+    assert(State.cursor1.pos > 1, 'bumped up against start of line')
     if Text.match(State.lines[State.cursor1.line].data, State.cursor1.pos-1, '%s') then
       break
     end
@@ -605,15 +605,14 @@ end
 
 function Text.match(s, pos, pat)
   local start_offset = Text.offset(s, pos)
-  assert(start_offset)
   local end_offset = Text.offset(s, pos+1)
-  assert(end_offset > start_offset)
+  assert(end_offset > start_offset, ('end_offset %d not > start_offset %d'):format(end_offset, start_offset))
   local curr = s:sub(start_offset, end_offset-1)
   return curr:match(pat)
 end
 
 function Text.left(State)
-  assert(State.lines[State.cursor1.line].mode == 'text')
+  assert(State.lines[State.cursor1.line].mode == 'text', 'line is not text')
   if State.cursor1.pos > 1 then
     State.cursor1.pos = State.cursor1.pos-1
   else
@@ -646,7 +645,7 @@ function Text.right(State)
 end
 
 function Text.right_without_scroll(State)
-  assert(State.lines[State.cursor1.line].mode == 'text')
+  assert(State.lines[State.cursor1.line].mode == 'text', 'line is not text')
   if State.cursor1.pos <= utf8.len(State.lines[State.cursor1.line].data) then
     State.cursor1.pos = State.cursor1.pos+1
   else
@@ -671,7 +670,7 @@ function Text.pos_at_start_of_screen_line(State, loc1)
       return spos,i
     end
   end
-  assert(false)
+  assert(false, ('invalid pos %d'):format(loc1.pos))
 end
 
 function Text.pos_at_end_of_screen_line(State, loc1)
@@ -685,7 +684,7 @@ function Text.pos_at_end_of_screen_line(State, loc1)
     end
     most_recent_final_pos = spos-1
   end
-  assert(false)
+  assert(false, ('invalid pos %d'):format(loc1.pos))
 end
 
 function Text.cursor_at_final_screen_line(State)
@@ -710,7 +709,7 @@ function Text.move_cursor_down_to_next_text_line_while_scrolling_again_if_necess
   end
   -- hack: insert a text line at bottom of file if necessary
   if State.cursor1.line > #State.lines then
-    assert(State.cursor1.line == #State.lines+1)
+    assert(State.cursor1.line == #State.lines+1, 'tried to ensure bottom line of file is text, but failed')
     table.insert(State.lines, {mode='text', data=''})
     table.insert(State.line_cache, {})
   end
@@ -742,8 +741,8 @@ function Text.snap_cursor_to_bottom_of_screen(State)
       end
       y = y - h
     else
-      assert(top2.line > 1)
-      assert(State.lines[top2.line-1].mode == 'drawing')
+      assert(top2.line > 1, 'tried to snap cursor to buttom of screen but failed')
+      assert(State.lines[top2.line-1].mode == 'drawing', "expected a drawing but it's not")
       -- We currently can't draw partial drawings, so either skip it entirely
       -- or not at all.
       local h = Drawing_padding_height + Drawing.pixels(State.lines[top2.line-1].h, State.width)
@@ -775,7 +774,7 @@ end
 function Text.to_pos_on_line(State, line_index, mx, my)
   local line = State.lines[line_index]
   local line_cache = State.line_cache[line_index]
-  assert(my >= line_cache.starty)
+  assert(my >= line_cache.starty, 'failed to map y pixel to line')
   -- duplicate some logic from Text.draw
   local y = line_cache.starty
   local start_screen_line_index = Text.screen_line_index(line_cache.screen_line_starting_pos, line_cache.startpos)
@@ -798,7 +797,7 @@ function Text.to_pos_on_line(State, line_index, mx, my)
     end
     y = nexty
   end
-  assert(false)
+  assert(false, 'failed to map y pixel to line')
 end
 
 function Text.screen_line_width(State, line_index, i)
@@ -864,7 +863,7 @@ function Text.nearest_cursor_pos(line, x, left)
       leftpos = curr
     end
   end
-  assert(false)
+  assert(false, 'failed to map x pixel to pos')
 end
 
 -- return the nearest index of line (in utf8 code points) which lies entirely
@@ -895,7 +894,7 @@ function Text.nearest_pos_less_than(line, x)
       left = curr
     end
   end
-  assert(false)
+  assert(false, 'failed to map x pixel to pos')
 end
 
 function Text.x_after(s, pos)
@@ -926,7 +925,7 @@ function Text.to2(State, loc1)
       break
     end
   end
-  assert(result.screen_pos)
+  assert(result.screen_pos, 'failed to convert schema-1 coordinate to schema-2')
   return result
 end
 
@@ -968,7 +967,7 @@ function Text.offset(s, pos1)
   if result == nil then
     print(pos1, #s, s)
   end
-  assert(result)
+  assert(result, "Text.offset returned nil; this is likely a failure to handle utf8")
   return result
 end
 
diff --git a/source_undo.lua b/source_undo.lua
index d3d5f0f..e5dea93 100644
--- a/source_undo.lua
+++ b/source_undo.lua
@@ -36,11 +36,11 @@ end
 -- Make copies of objects; the rest of the app may mutate them in place, but undo requires immutable histories.
 function snapshot(State, s,e)
   -- Snapshot everything by default, but subset if requested.
-  assert(s)
+  assert(s, 'failed to snapshot operation for undo history')
   if e == nil then
     e = s
   end
-  assert(#State.lines > 0)
+  assert(#State.lines > 0, 'failed to snapshot operation for undo history')
   if s < 1 then s = 1 end
   if s > #State.lines then s = #State.lines end
   if e < 1 then e = 1 end
@@ -65,8 +65,7 @@ function snapshot(State, s,e)
     elseif line.mode == 'drawing' then
       table.insert(event.lines, {mode='drawing', h=line.h, points=deepcopy(line.points), shapes=deepcopy(line.shapes), pending={}})
     else
-      print(line.mode)
-      assert(false)
+      assert(false, ('unknown line mode %s'):format(line.mode))
     end
   end
   return event
@@ -80,22 +79,22 @@ function patch(lines, from, to)
 --?     lines[from.start_line] = to.lines[1]
 --?     return
 --?   end
-  assert(from.start_line == to.start_line)
+  assert(from.start_line == to.start_line, 'failed to patch undo operation')
   for i=from.end_line,from.start_line,-1 do
     table.remove(lines, i)
   end
-  assert(#to.lines == to.end_line-to.start_line+1)
+  assert(#to.lines == to.end_line-to.start_line+1, 'failed to patch undo operation')
   for i=1,#to.lines do
     table.insert(lines, to.start_line+i-1, to.lines[i])
   end
 end
 
 function patch_placeholders(line_cache, from, to)
-  assert(from.start_line == to.start_line)
+  assert(from.start_line == to.start_line, 'failed to patch undo operation')
   for i=from.end_line,from.start_line,-1 do
     table.remove(line_cache, i)
   end
-  assert(#to.lines == to.end_line-to.start_line+1)
+  assert(#to.lines == to.end_line-to.start_line+1, 'failed to patch undo operation')
   for i=1,#to.lines do
     table.insert(line_cache, to.start_line+i-1, {})
   end
diff --git a/text.lua b/text.lua
index 3323af6..cd80464 100644
--- a/text.lua
+++ b/text.lua
@@ -12,7 +12,7 @@ function Text.draw(State, line_index, y, startpos)
   -- wrap long lines
   local final_screen_line_starting_pos = startpos  -- track value to return
   Text.populate_screen_line_starting_pos(State, line_index)
-  assert(#line_cache.screen_line_starting_pos >= 1)
+  assert(#line_cache.screen_line_starting_pos >= 1, 'line cache missing screen line info')
   for i=1,#line_cache.screen_line_starting_pos do
     local pos = line_cache.screen_line_starting_pos[i]
     if pos < startpos then
@@ -135,7 +135,7 @@ function Text.text_input(State, t)
 end
 
 function Text.insert_at_cursor(State, t)
-  assert(State.lines[State.cursor1.line].mode == 'text')
+  assert(State.lines[State.cursor1.line].mode == 'text', 'line is not text')
   local byte_offset = Text.offset(State.lines[State.cursor1.line].data, State.cursor1.pos)
   State.lines[State.cursor1.line].data = string.sub(State.lines[State.cursor1.line].data, 1, byte_offset-1)..t..string.sub(State.lines[State.cursor1.line].data, byte_offset)
   Text.clear_screen_line_cache(State, State.cursor1.line)
@@ -212,7 +212,7 @@ function Text.keychord_press(State, chord)
       Text.redraw_all(State)  -- if we're scrolling, reclaim all fragments to avoid memory leaks
     end
     Text.clear_screen_line_cache(State, State.cursor1.line)
-    assert(Text.le1(State.screen_top1, State.cursor1))
+    assert(Text.le1(State.screen_top1, State.cursor1), ('screen_top (line=%d,pos=%d) is below cursor (line=%d,pos=%d)'):format(State.screen_top1.line, State.screen_top1.pos, State.cursor1.line, State.cursor1.pos))
     schedule_save(State)
     record_undo_event(State, {before=before, after=snapshot(State, State.cursor1.line)})
   elseif chord == 'delete' then
@@ -390,7 +390,7 @@ function Text.pagedown(State)
 end
 
 function Text.up(State)
-  assert(State.lines[State.cursor1.line].mode == 'text')
+  assert(State.lines[State.cursor1.line].mode == 'text', 'line is not text')
 --?   print('up', State.cursor1.line, State.cursor1.pos, State.screen_top1.line, State.screen_top1.pos)
   local screen_line_starting_pos, screen_line_index = Text.pos_at_start_of_screen_line(State, State.cursor1)
   if screen_line_starting_pos == 1 then
@@ -416,7 +416,7 @@ function Text.up(State)
     end
   else
     -- move up one screen line in current line
-    assert(screen_line_index > 1)
+    assert(screen_line_index > 1, 'bumped up against top screen line in line')
     local new_screen_line_starting_pos = State.line_cache[State.cursor1.line].screen_line_starting_pos[screen_line_index-1]
     local new_screen_line_starting_byte_offset = Text.offset(State.lines[State.cursor1.line].data, new_screen_line_starting_pos)
     local s = string.sub(State.lines[State.cursor1.line].data, new_screen_line_starting_byte_offset)
@@ -433,9 +433,9 @@ function Text.up(State)
 end
 
 function Text.down(State)
-  assert(State.lines[State.cursor1.line].mode == 'text')
+  assert(State.lines[State.cursor1.line].mode == 'text', 'line is not text')
 --?   print('down', State.cursor1.line, State.cursor1.pos, State.screen_top1.line, State.screen_top1.pos, State.screen_bottom1.line, State.screen_bottom1.pos)
-  assert(State.cursor1.pos)
+  assert(State.cursor1.pos, 'cursor has no pos')
   if Text.cursor_at_final_screen_line(State) then
     -- line is done, skip to next text line
 --?     print('cursor at final screen line of its line')
@@ -509,7 +509,7 @@ function Text.word_left(State)
     if State.cursor1.pos == 1 then
       break
     end
-    assert(State.cursor1.pos > 1)
+    assert(State.cursor1.pos > 1, 'bumped up against start of line')
     if Text.match(State.lines[State.cursor1.line].data, State.cursor1.pos-1, '%s') then
       break
     end
@@ -543,15 +543,14 @@ end
 
 function Text.match(s, pos, pat)
   local start_offset = Text.offset(s, pos)
-  assert(start_offset)
   local end_offset = Text.offset(s, pos+1)
-  assert(end_offset > start_offset)
+  assert(end_offset > start_offset, ('end_offset %d not > start_offset %d'):format(end_offset, start_offset))
   local curr = s:sub(start_offset, end_offset-1)
   return curr:match(pat)
 end
 
 function Text.left(State)
-  assert(State.lines[State.cursor1.line].mode == 'text')
+  assert(State.lines[State.cursor1.line].mode == 'text', 'line is not text')
   if State.cursor1.pos > 1 then
     State.cursor1.pos = State.cursor1.pos-1
   else
@@ -584,7 +583,7 @@ function Text.right(State)
 end
 
 function Text.right_without_scroll(State)
-  assert(State.lines[State.cursor1.line].mode == 'text')
+  assert(State.lines[State.cursor1.line].mode == 'text', 'line is not text')
   if State.cursor1.pos <= utf8.len(State.lines[State.cursor1.line].data) then
     State.cursor1.pos = State.cursor1.pos+1
   else
@@ -609,7 +608,7 @@ function Text.pos_at_start_of_screen_line(State, loc1)
       return spos,i
     end
   end
-  assert(false)
+  assert(false, ('invalid pos %d'):format(loc1.pos))
 end
 
 function Text.pos_at_end_of_screen_line(State, loc1)
@@ -623,7 +622,7 @@ function Text.pos_at_end_of_screen_line(State, loc1)
     end
     most_recent_final_pos = spos-1
   end
-  assert(false)
+  assert(false, ('invalid pos %d'):format(loc1.pos))
 end
 
 function Text.cursor_at_final_screen_line(State)
@@ -648,7 +647,7 @@ function Text.move_cursor_down_to_next_text_line_while_scrolling_again_if_necess
   end
   -- hack: insert a text line at bottom of file if necessary
   if State.cursor1.line > #State.lines then
-    assert(State.cursor1.line == #State.lines+1)
+    assert(State.cursor1.line == #State.lines+1, 'tried to ensure bottom line of file is text, but failed')
     table.insert(State.lines, {mode='text', data=''})
     table.insert(State.line_cache, {})
   end
@@ -680,8 +679,8 @@ function Text.snap_cursor_to_bottom_of_screen(State)
       end
       y = y - h
     else
-      assert(top2.line > 1)
-      assert(State.lines[top2.line-1].mode == 'drawing')
+      assert(top2.line > 1, 'tried to snap cursor to buttom of screen but failed')
+      assert(State.lines[top2.line-1].mode == 'drawing', "expected a drawing but it's not")
       -- We currently can't draw partial drawings, so either skip it entirely
       -- or not at all.
       local h = Drawing_padding_height + Drawing.pixels(State.lines[top2.line-1].h, State.width)
@@ -713,7 +712,7 @@ end
 function Text.to_pos_on_line(State, line_index, mx, my)
   local line = State.lines[line_index]
   local line_cache = State.line_cache[line_index]
-  assert(my >= line_cache.starty)
+  assert(my >= line_cache.starty, 'failed to map y pixel to line')
   -- duplicate some logic from Text.draw
   local y = line_cache.starty
   local start_screen_line_index = Text.screen_line_index(line_cache.screen_line_starting_pos, line_cache.startpos)
@@ -736,7 +735,7 @@ function Text.to_pos_on_line(State, line_index, mx, my)
     end
     y = nexty
   end
-  assert(false)
+  assert(false, 'failed to map y pixel to line')
 end
 
 function Text.screen_line_width(State, line_index, i)
@@ -802,7 +801,7 @@ function Text.nearest_cursor_pos(line, x, left)
       leftpos = curr
     end
   end
-  assert(false)
+  assert(false, 'failed to map x pixel to pos')
 end
 
 -- return the nearest index of line (in utf8 code points) which lies entirely
@@ -833,7 +832,7 @@ function Text.nearest_pos_less_than(line, x)
       left = curr
     end
   end
-  assert(false)
+  assert(false, 'failed to map x pixel to pos')
 end
 
 function Text.x_after(s, pos)
@@ -864,7 +863,7 @@ function Text.to2(State, loc1)
       break
     end
   end
-  assert(result.screen_pos)
+  assert(result.screen_pos, 'failed to convert schema-1 coordinate to schema-2')
   return result
 end
 
@@ -906,7 +905,7 @@ function Text.offset(s, pos1)
   if result == nil then
     print(pos1, #s, s)
   end
-  assert(result)
+  assert(result, "Text.offset returned nil; this is likely a failure to handle utf8")
   return result
 end
 
diff --git a/undo.lua b/undo.lua
index d3d5f0f..1ed3ce7 100644
--- a/undo.lua
+++ b/undo.lua
@@ -36,11 +36,11 @@ end
 -- Make copies of objects; the rest of the app may mutate them in place, but undo requires immutable histories.
 function snapshot(State, s,e)
   -- Snapshot everything by default, but subset if requested.
-  assert(s)
+  assert(s, 'failed to snapshot operation for undo history')
   if e == nil then
     e = s
   end
-  assert(#State.lines > 0)
+  assert(#State.lines > 0, 'failed to snapshot operation for undo history')
   if s < 1 then s = 1 end
   if s > #State.lines then s = #State.lines end
   if e < 1 then e = 1 end
@@ -66,7 +66,7 @@ function snapshot(State, s,e)
       table.insert(event.lines, {mode='drawing', h=line.h, points=deepcopy(line.points), shapes=deepcopy(line.shapes), pending={}})
     else
       print(line.mode)
-      assert(false)
+      assert(false, ('unknown line mode %s'):format(line.mode))
     end
   end
   return event
@@ -80,22 +80,22 @@ function patch(lines, from, to)
 --?     lines[from.start_line] = to.lines[1]
 --?     return
 --?   end
-  assert(from.start_line == to.start_line)
+  assert(from.start_line == to.start_line, 'failed to patch undo operation')
   for i=from.end_line,from.start_line,-1 do
     table.remove(lines, i)
   end
-  assert(#to.lines == to.end_line-to.start_line+1)
+  assert(#to.lines == to.end_line-to.start_line+1, 'failed to patch undo operation')
   for i=1,#to.lines do
     table.insert(lines, to.start_line+i-1, to.lines[i])
   end
 end
 
 function patch_placeholders(line_cache, from, to)
-  assert(from.start_line == to.start_line)
+  assert(from.start_line == to.start_line, 'failed to patch undo operation')
   for i=from.end_line,from.start_line,-1 do
     table.remove(line_cache, i)
   end
-  assert(#to.lines == to.end_line-to.start_line+1)
+  assert(#to.lines == to.end_line-to.start_line+1, 'failed to patch undo operation')
   for i=1,#to.lines do
     table.insert(line_cache, to.start_line+i-1, {})
   end