From eb68e9fd67fcb94c4c6c20b1dc3414fc56f79e27 Mon Sep 17 00:00:00 2001 From: Benjamin Date: Sat, 26 Oct 2024 22:24:03 -0500 Subject: [PATCH] introduce custom grid type --- squared_away/src/squared_away.gleam | 59 +++++-------- squared_away_lang/src/squared_away_lang.gleam | 73 ++++++---------- .../src/squared_away_lang/grid.gleam | 84 +++++++++++++++++++ .../src/squared_away_lang/interpreter.gleam | 29 +++---- .../src/squared_away_lang/typechecker.gleam | 46 +++++----- .../typechecker/typed_expr.gleam | 3 +- .../src/squared_away_lang/util.gleam | 17 ---- .../test/squared_away_lang_test.gleam | 66 --------------- 8 files changed, 170 insertions(+), 207 deletions(-) create mode 100644 squared_away_lang/src/squared_away_lang/grid.gleam delete mode 100644 squared_away_lang/src/squared_away_lang/util.gleam diff --git a/squared_away/src/squared_away.gleam b/squared_away/src/squared_away.gleam index 65ffb34..d7b6530 100644 --- a/squared_away/src/squared_away.gleam +++ b/squared_away/src/squared_away.gleam @@ -13,6 +13,7 @@ import lustre/event import pprint import squared_away_lang as lang import squared_away_lang/error +import squared_away_lang/grid import squared_away_lang/interpreter/value import squared_away_lang/typechecker/typ import squared_away_lang/typechecker/type_error @@ -32,25 +33,16 @@ pub fn main() { type Model { Model( formula_mode: Bool, - active_cell: Option(String), - src_grid: Dict(String, String), - value_grid: Dict(String, Result(value.Value, error.CompileError)), - errors_to_display: List(#(String, error.CompileError)), + active_cell: Option(grid.GridKey), + src_grid: grid.Grid(String), + value_grid: grid.Grid(Result(value.Value, error.CompileError)), + errors_to_display: List(#(grid.GridKey, error.CompileError)), ) } fn init(_flags) -> #(Model, effect.Effect(Msg)) { - let cols = list.range(1, grid_width) - let rows = list.range(1, grid_height) - - let src_grid = - list.fold(cols, dict.new(), fn(grid, c) { - list.fold(rows, dict.new(), fn(partial_grid, r) { - let key = int.to_string(c) <> "_" <> int.to_string(r) - partial_grid |> dict.insert(key, "") - }) - |> dict.merge(grid) - }) + let src_grid = grid.new(grid_width, grid_height, "") + let value_grid = grid.new(grid_width, grid_height, Ok(value.Empty)) // We could presume that our value_grid starts with all empty, // but instead I think we should scan, parse, typecheck, and @@ -61,7 +53,7 @@ fn init(_flags) -> #(Model, effect.Effect(Msg)) { formula_mode: False, active_cell: None, src_grid:, - value_grid: dict.new(), + value_grid:, errors_to_display: [], ) |> update_grid @@ -71,8 +63,8 @@ fn init(_flags) -> #(Model, effect.Effect(Msg)) { type Msg { UserToggledFormulaMode(to: Bool) - UserSetCellValue(key: String, val: String) - UserFocusedOnCell(key: String) + UserSetCellValue(key: grid.GridKey, val: String) + UserFocusedOnCell(key: grid.GridKey) UserFocusedOffCell } @@ -84,7 +76,7 @@ fn update_grid(model: Model) -> Model { // Loop over the grid to see if there's any errors to display let errors_to_display = - dict.fold(value_grid, [], fn(acc, key, val) { + grid.fold(value_grid, [], fn(acc, key, val) { case val { Error(err) -> [#(key, err), ..acc] Ok(_) -> acc @@ -98,7 +90,7 @@ fn update(model: Model, msg: Msg) -> #(Model, effect.Effect(Msg)) { case msg { UserSetCellValue(key, val) -> { let model = - Model(..model, src_grid: dict.insert(model.src_grid, key, val)) + Model(..model, src_grid: grid.insert(model.src_grid, key, val)) #(update_grid(model), effect.none()) } UserToggledFormulaMode(formula_mode) -> #( @@ -127,12 +119,11 @@ fn view(model: Model) -> element.Element(Msg) { |> result.unwrap(or: html.div([], [])) let rows = - list.range(1, grid_height) - |> list.map(int.to_string) - |> list.map(fn(row) { + model.src_grid.cells + |> list.group(grid.row) + |> dict.map_values(fn(_, keys) { let cells = - list.map(columns, fn(col) { - let key = col <> "_" <> row + list.map(keys, fn(key) { let on_input = event.on_input(UserSetCellValue(key:, val: _)) let out_of_focus = event.on_blur(UserFocusedOffCell) let on_focus = event.on_focus(UserFocusedOnCell(key)) @@ -140,20 +131,11 @@ fn view(model: Model) -> element.Element(Msg) { model.active_cell == Some(key) || model.formula_mode let value = case show_formula { - True -> - case dict.get(model.src_grid, key) { - Error(_) -> no_value_found_txt() - Ok(src) -> src - } + True -> grid.get(model.src_grid, key) False -> - case dict.get(model.value_grid, key) { - Error(_) -> no_value_found_txt() - Ok(v) -> { - case v { - Error(e) -> error.error_type_string(e) - Ok(v) -> value.value_to_string(v) - } - } + case grid.get(model.value_grid, key) { + Error(e) -> error.error_type_string(e) + Ok(v) -> value.value_to_string(v) } } |> attribute.value @@ -172,6 +154,7 @@ fn view(model: Model) -> element.Element(Msg) { html.tr([], cells) }) + |> dict.values let grid = html.div([class("table-container")], [ diff --git a/squared_away_lang/src/squared_away_lang.gleam b/squared_away_lang/src/squared_away_lang.gleam index 42ba805..85e60bb 100644 --- a/squared_away_lang/src/squared_away_lang.gleam +++ b/squared_away_lang/src/squared_away_lang.gleam @@ -4,6 +4,7 @@ import gleam/list import gleam/option.{None, Some} import gleam/result import squared_away_lang/error +import squared_away_lang/grid import squared_away_lang/interpreter import squared_away_lang/interpreter/value import squared_away_lang/parser @@ -13,82 +14,62 @@ import squared_away_lang/scanner/token import squared_away_lang/typechecker import squared_away_lang/typechecker/typ import squared_away_lang/typechecker/typed_expr -import squared_away_lang/util pub fn interpret_grid( - input: dict.Dict(String, Result(typed_expr.TypedExpr, error.CompileError)), -) -> dict.Dict(String, Result(value.Value, error.CompileError)) { - use acc, key, typed_expr <- dict.fold(input, dict.new()) + input: grid.Grid(Result(typed_expr.TypedExpr, error.CompileError)), +) -> grid.Grid(Result(value.Value, error.CompileError)) { + use _, typed_expr <- grid.map_values(input) case typed_expr { - Error(e) -> dict.insert(acc, key, Error(e)) - Ok(typed_expr) -> { - let maybe_value = interpreter.interpret(input, typed_expr) - dict.insert(acc, key, maybe_value) - } + Error(e) -> Error(e) + Ok(typed_expr) -> interpreter.interpret(input, typed_expr) } } pub fn typecheck_grid( - input: dict.Dict(String, Result(expr.Expr, error.CompileError)), -) -> dict.Dict(String, Result(typed_expr.TypedExpr, error.CompileError)) { - use acc, key, expr <- dict.fold(input, dict.new()) + input: grid.Grid(Result(expr.Expr, error.CompileError)), +) -> grid.Grid(Result(typed_expr.TypedExpr, error.CompileError)) { + use key, expr <- grid.map_values(input) case expr { - Error(e) -> dict.insert(acc, key, Error(e)) + Error(e) -> Error(e) Ok(expr.Label(txt)) -> { - case util.cell_to_the_right(key) { - None -> - dict.insert(acc, key, Ok(typed_expr.Label(type_: typ.TNil, txt:))) - Some(new_key) -> { - let val = case dict.get(input, new_key) { - Error(_) -> { - io.debug( - "Unexpected uninitialized cell encountered suring typechecking", - ) - panic - } - Ok(v) -> v - } + case grid.cell_to_the_right(input, key) { + Error(Nil) -> Ok(typed_expr.Label(type_: typ.TNil, txt:)) + Ok(new_key) -> { + let val = grid.get(input, new_key) case val { - Error(e) -> dict.insert(acc, key, Error(e)) + Error(e) -> Error(e) Ok(val) -> { case typechecker.typecheck(input, val) { - Error(e) -> dict.insert(acc, key, Error(e)) + Error(e) -> Error(e) Ok(typed_val) -> - dict.insert( - acc, - key, - Ok(typed_expr.Label(type_: typed_val.type_, txt:)), - ) + Ok(typed_expr.Label(type_: typed_val.type_, txt:)) } } } } } } - Ok(expr) -> { - let maybe_typed_expr = typechecker.typecheck(input, expr) - dict.insert(acc, key, maybe_typed_expr) - } + Ok(expr) -> typechecker.typecheck(input, expr) } } pub fn parse_grid( - input: dict.Dict(String, Result(List(token.Token), error.CompileError)), -) -> dict.Dict(String, Result(expr.Expr, error.CompileError)) { - use acc, key, toks <- dict.fold(input, dict.new()) + input: grid.Grid(Result(List(token.Token), error.CompileError)), +) -> grid.Grid(Result(expr.Expr, error.CompileError)) { + use _, toks <- grid.map_values(input) case toks { - Error(e) -> dict.insert(acc, key, Error(e)) + Error(e) -> Error(e) Ok(toks) -> { let expr = parser.parse(toks) - dict.insert(acc, key, expr |> result.map_error(error.ParseError)) + expr |> result.map_error(error.ParseError) } } } pub fn scan_grid( - input: dict.Dict(String, String), -) -> dict.Dict(String, Result(List(token.Token), error.CompileError)) { - use acc, key, src <- dict.fold(input, dict.new()) + input: grid.Grid(String), +) -> grid.Grid(Result(List(token.Token), error.CompileError)) { + use _, src <- grid.map_values(input) let maybe_scanned = scanner.scan(src) |> result.map_error(error.ScanError) @@ -98,5 +79,5 @@ pub fn scan_grid( _ -> t } })) - dict.insert(acc, key, maybe_scanned) + maybe_scanned } diff --git a/squared_away_lang/src/squared_away_lang/grid.gleam b/squared_away_lang/src/squared_away_lang/grid.gleam new file mode 100644 index 0000000..e494618 --- /dev/null +++ b/squared_away_lang/src/squared_away_lang/grid.gleam @@ -0,0 +1,84 @@ +//// Grid operations are kind of a pain to get right / can produce some +//// boilerplate around results for get operations, so I'm gonna try and extract +//// them to a module + +import gleam/dict +import gleam/list +import gleam/option.{type Option, None, Some} + +// Making the type generic since we do a grid of src +// and a grid of interpreted values +pub type Grid(a) { + Grid(inner: dict.Dict(GridKey, a), cells: List(GridKey)) +} + +pub opaque type GridKey { + GridKey(row: Int, col: Int) +} + +pub fn row(grid_key: GridKey) -> Int { + grid_key.row +} + +pub fn col(grid_key: GridKey) -> Int { + grid_key.col +} + +pub fn new(width: Int, height: Int, default: a) -> Grid(a) { + let cols = list.range(1, width) + let rows = list.range(1, height) + + let #(g, c) = + list.fold(rows, #(dict.new(), []), fn(acc, row) { + let #(grid, cells) = acc + + let #(g, c) = + list.fold(cols, #(dict.new(), []), fn(acc, col) { + let #(g, c) = acc + #(dict.insert(g, GridKey(row:, col:), default), [ + GridKey(row:, col:), + ..c + ]) + }) + + #(dict.merge(grid, g), list.concat([c, cells])) + }) + + Grid(g, c) +} + +pub fn insert(grid: Grid(a), key: GridKey, item: a) -> Grid(a) { + Grid(..grid, inner: dict.insert(grid.inner, key, item)) +} + +pub fn fold(grid: Grid(a), acc: b, do: fn(b, GridKey, a) -> b) -> b { + dict.fold(grid.inner, acc, do) +} + +pub fn map_values(grid: Grid(a), do: fn(GridKey, a) -> b) -> Grid(b) { + Grid(inner: dict.map_values(grid.inner, do), cells: grid.cells) +} + +pub fn get(grid: Grid(a), key: GridKey) -> a { + // Because the `GridKey` is an opaque type produced + // by creating the grid, the get operation is safe. + let assert Ok(item) = dict.get(grid.inner, key) + item +} + +pub fn cell_to_the_right(grid: Grid(a), key: GridKey) -> Result(GridKey, Nil) { + list.find(grid.cells, fn(k) { k.row == key.row && k.col == key.col + 1 }) +} + +pub fn intersect(row_cell: GridKey, col_cell: GridKey) -> Result(GridKey, Nil) { + let GridKey(row, col_check) = row_cell + let GridKey(row_check, col) = col_cell + case row != row_check && col != col_check { + False -> Error(Nil) + True -> Ok(GridKey(row:, col:)) + } +} + +pub fn to_list(grid: Grid(a)) -> List(#(GridKey, a)) { + dict.to_list(grid.inner) +} diff --git a/squared_away_lang/src/squared_away_lang/interpreter.gleam b/squared_away_lang/src/squared_away_lang/interpreter.gleam index 2d24ae0..9a1ad33 100644 --- a/squared_away_lang/src/squared_away_lang/interpreter.gleam +++ b/squared_away_lang/src/squared_away_lang/interpreter.gleam @@ -5,14 +5,14 @@ import gleam/list.{Continue, Stop} import gleam/option.{None, Some} import gleam/result import squared_away_lang/error +import squared_away_lang/grid import squared_away_lang/interpreter/runtime_error import squared_away_lang/interpreter/value import squared_away_lang/parser/expr import squared_away_lang/typechecker/typed_expr -import squared_away_lang/util pub fn interpret( - env: dict.Dict(String, Result(typed_expr.TypedExpr, error.CompileError)), + env: grid.Grid(Result(typed_expr.TypedExpr, error.CompileError)), expr: typed_expr.TypedExpr, ) -> Result(value.Value, error.CompileError) { case expr { @@ -20,19 +20,15 @@ pub fn interpret( typed_expr.LabelDef(_, txt) -> Ok(value.Text(txt)) typed_expr.Group(_, expr) -> interpret(env, expr) typed_expr.CrossLabel(x, key) -> { - case dict.get(env, key) { - Ok(expr) -> - case expr { - Error(e) -> Error(e) - Ok(expr) -> interpret(env, expr) - } + case grid.get(env, key) { + Ok(expr) -> interpret(env, expr) Error(_) -> Ok(value.Empty) } } typed_expr.Label(_, txt) -> { let key = env - |> dict.to_list + |> grid.to_list |> list.fold_until(None, fn(_, i) { case i { #(cell_ref, Ok(typed_expr.LabelDef(_, label_txt))) @@ -43,7 +39,8 @@ pub fn interpret( _ -> Continue(None) } }) - |> option.map(util.cell_to_the_right) + |> option.map(grid.cell_to_the_right(env, _)) + |> option.map(option.from_result) |> option.flatten case key { @@ -54,15 +51,9 @@ pub fn interpret( )), ) Some(key) -> { - case dict.get(env, key) { - Error(Nil) -> - Error( - error.RuntimeError(runtime_error.RuntimeError( - "Label doesn't point to anything", - )), - ) - Ok(Error(e)) -> Error(e) - Ok(Ok(te)) -> { + case grid.get(env, key) { + Error(e) -> Error(e) + Ok(te) -> { interpret(env, te) } } diff --git a/squared_away_lang/src/squared_away_lang/typechecker.gleam b/squared_away_lang/src/squared_away_lang/typechecker.gleam index 1e2baeb..135cb38 100644 --- a/squared_away_lang/src/squared_away_lang/typechecker.gleam +++ b/squared_away_lang/src/squared_away_lang/typechecker.gleam @@ -5,14 +5,14 @@ import gleam/option.{None, Some} import gleam/result import gleam/string import squared_away_lang/error +import squared_away_lang/grid import squared_away_lang/parser/expr import squared_away_lang/typechecker/typ import squared_away_lang/typechecker/type_error import squared_away_lang/typechecker/typed_expr -import squared_away_lang/util pub fn typecheck( - env: dict.Dict(String, Result(expr.Expr, error.CompileError)), + env: grid.Grid(Result(expr.Expr, error.CompileError)), expr: expr.Expr, ) -> Result(typed_expr.TypedExpr, error.CompileError) { case expr { @@ -24,7 +24,7 @@ pub fn typecheck( expr.Label(txt) -> { let key = env - |> dict.to_list + |> grid.to_list |> list.fold_until(None, fn(_, i) { case i { #(cell_ref, Ok(expr.LabelDef(label_txt))) if label_txt == txt -> { @@ -33,13 +33,14 @@ pub fn typecheck( _ -> Continue(None) } }) - |> option.map(util.cell_to_the_right) + |> option.map(grid.cell_to_the_right(env, _)) + |> option.map(option.from_result) |> option.flatten case key { None -> Ok(typed_expr.Label(typ.TNil, txt)) Some(key) -> { - let assert Ok(x) = dict.get(env, key) + let x = grid.get(env, key) case x { Error(e) -> Error(e) Ok(expr) -> { @@ -55,7 +56,7 @@ pub fn typecheck( expr.CrossLabel(row:, col:) -> { let col_cell = env - |> dict.to_list + |> grid.to_list |> list.fold_until(None, fn(_, cell) { case cell { #(cell_ref, Ok(expr.LabelDef(label_txt))) if label_txt == col -> { @@ -71,13 +72,9 @@ pub fn typecheck( error.TypeError(type_error.TypeError("No label called: " <> col)), ) Some(col_cell) -> { - let assert Ok(#(col, _)) = - col_cell - |> string.split_once("_") - let row_cell = env - |> dict.to_list + |> grid.to_list |> list.fold_until(None, fn(_, i) { case i { #(cell_ref, Ok(expr.LabelDef(label_txt))) if label_txt == row -> { @@ -93,16 +90,25 @@ pub fn typecheck( error.TypeError(type_error.TypeError("No label called: " <> row)), ) Some(row_cell) -> { - let assert Ok(#(_, row)) = row_cell |> string.split_once("_") - let key = col <> "_" <> row - - let assert Ok(x) = dict.get(env, key) - case x { - Error(e) -> Error(e) - Ok(expr) -> { - case typecheck(env, expr) { - Ok(te) -> Ok(typed_expr.CrossLabel(type_: te.type_, key:)) + let new_key = grid.intersect(row_cell, col_cell) + case new_key { + Error(_) -> + Error( + error.TypeError(type_error.TypeError( + "Labels " <> row <> " and " <> col <> " do not intersect", + )), + ) + Ok(nk) -> { + let x = grid.get(env, nk) + case x { Error(e) -> Error(e) + Ok(expr) -> { + case typecheck(env, expr) { + Ok(te) -> + Ok(typed_expr.CrossLabel(type_: te.type_, key: nk)) + Error(e) -> Error(e) + } + } } } } diff --git a/squared_away_lang/src/squared_away_lang/typechecker/typed_expr.gleam b/squared_away_lang/src/squared_away_lang/typechecker/typed_expr.gleam index 2711687..65d59fe 100644 --- a/squared_away_lang/src/squared_away_lang/typechecker/typed_expr.gleam +++ b/squared_away_lang/src/squared_away_lang/typechecker/typed_expr.gleam @@ -1,3 +1,4 @@ +import squared_away_lang/grid import squared_away_lang/parser/expr import squared_away_lang/typechecker/typ @@ -5,7 +6,7 @@ pub type TypedExpr { Empty(type_: typ.Typ) FloatLiteral(type_: typ.Typ, f: Float) Label(type_: typ.Typ, txt: String) - CrossLabel(type_: typ.Typ, key: String) + CrossLabel(type_: typ.Typ, key: grid.GridKey) LabelDef(type_: typ.Typ, txt: String) IntegerLiteral(type_: typ.Typ, n: Int) BooleanLiteral(type_: typ.Typ, b: Bool) diff --git a/squared_away_lang/src/squared_away_lang/util.gleam b/squared_away_lang/src/squared_away_lang/util.gleam deleted file mode 100644 index 9bc810f..0000000 --- a/squared_away_lang/src/squared_away_lang/util.gleam +++ /dev/null @@ -1,17 +0,0 @@ -import gleam/int -import gleam/option.{type Option, None, Some} -import gleam/string - -pub fn cell_to_the_right(input: String) -> Option(String) { - // A cell reference is s column number and then a row number, - // separated by and _. - // We can get the cell to the right by splitting the col off, incrementing - // it, then adding the row number back - - let assert Ok(#(col, row)) = string.split_once(input, "_") - let assert Ok(col_num) = int.parse(col) - case col_num >= 5 { - True -> None - False -> Some(int.to_string(col_num + 1) <> "_" <> row) - } -} diff --git a/squared_away_lang/test/squared_away_lang_test.gleam b/squared_away_lang/test/squared_away_lang_test.gleam index 5989df6..fa86dc5 100644 --- a/squared_away_lang/test/squared_away_lang_test.gleam +++ b/squared_away_lang/test/squared_away_lang_test.gleam @@ -11,69 +11,3 @@ import squared_away_lang/interpreter/value pub fn main() { gleeunit.main() } - -fn empty_grid() -> dict.Dict(String, String) { - let cols = list.range(1, 5) - let rows = list.range(1, 5) - - list.fold(cols, dict.new(), fn(grid, c) { - list.fold(rows, dict.new(), fn(partial_grid, r) { - let key = int.to_string(c) <> "_" <> int.to_string(r) - partial_grid |> dict.insert(key, "") - }) - |> dict.merge(grid) - }) -} - -fn print_grid_values( - grid: dict.Dict(String, Result(value.Value, error.CompileError)), - keys: List(String), -) -> String { - use acc, key, val <- dict.fold(grid, "") - case list.contains(keys, key) { - False -> acc - True -> - case val { - Ok(v) -> acc <> key <> ": " <> value.value_to_string(v) <> "\n" - Error(e) -> acc <> key <> ": " <> string.inspect(e) - } - } -} - -// gleeunit test functions end in `_test` -pub fn basic_label_usage_test() { - let grid = - empty_grid() - |> dict.insert("1_1", "X") - |> dict.insert("2_1", "=4") - |> dict.insert("2_2", "=X") - - let res = { - let scanned = lang.scan_grid(grid) - let parsed = lang.parse_grid(scanned) - let typechecked = lang.typecheck_grid(parsed) - lang.interpret_grid(typechecked) - } - - print_grid_values(res, ["1_1", "2_1", "2_2"]) - |> birdie.snap(title: "Basic Label Usage") -} - -pub fn parse_cross_ref_test() { - let grid = - empty_grid() - |> dict.insert("1_2", "Ben") - |> dict.insert("2_1", "Height") - |> dict.insert("2_2", "=4") - |> dict.insert("3_2", "=Ben_Height") - - let res = { - let scanned = lang.scan_grid(grid) - let parsed = lang.parse_grid(scanned) - let typechecked = lang.typecheck_grid(parsed) - lang.interpret_grid(typechecked) - } - - print_grid_values(res, ["1_2", "2_1", "2_2", "3_2"]) - |> birdie.snap(title: "Parse Cross Reference") -}