diff --git a/Makefile b/Makefile index 524e8be..fd59e09 100644 --- a/Makefile +++ b/Makefile @@ -10,4 +10,8 @@ review: cd squared_away_lang && gleam run -m birdie dev: - cd squared_away && gleam run -m lustre/dev start \ No newline at end of file + cd squared_away && gleam run -m lustre/dev start + +clean: + cd squared_away && gleam clean + cd squared_away_lang && gleam clean \ No newline at end of file diff --git a/squared_away/index.html b/squared_away/index.html index f064f93..a27b965 100644 --- a/squared_away/index.html +++ b/squared_away/index.html @@ -1,18 +1,20 @@ - - - - - 🚧 form + + + + - - - - + 🚧 form - -
- - + + + + + + +
+ + + \ No newline at end of file diff --git a/squared_away/src/squared_away.gleam b/squared_away/src/squared_away.gleam index 44b1a5e..ca5a50e 100644 --- a/squared_away/src/squared_away.gleam +++ b/squared_away/src/squared_away.gleam @@ -1,15 +1,17 @@ -import renderable_error import gleam/dict import gleam/int +import gleam/io import gleam/list import gleam/option.{type Option, None, Some} import gleam/result +import gleam/string import lustre import lustre/attribute.{class} import lustre/effect import lustre/element import lustre/element/html import lustre/event +import renderable_error import squared_away_lang as lang import squared_away_lang/error import squared_away_lang/grid @@ -28,12 +30,15 @@ pub fn main() { Nil } +@external(javascript, "./squared_away_ffi.js", "focus") +fn focus(id: String) -> Nil + /// To start, our model will be a 5x5 grid of Strings type Model { Model( grid_width: Int, grid_height: Int, - formula_mode: Bool, + display_mode: DisplayMode, active_cell: Option(grid.GridKey), src_grid: grid.Grid(String), value_grid: grid.Grid(Result(value.Value, error.CompileError)), @@ -41,6 +46,12 @@ type Model { ) } +type DisplayMode { + DisplayValues + DisplayFormulas + DisplayGridCoords +} + fn init(_flags) -> #(Model, effect.Effect(Msg)) { let src_grid = grid.new(initial_grid_width, initial_grid_height, "") let value_grid = @@ -50,7 +61,7 @@ fn init(_flags) -> #(Model, effect.Effect(Msg)) { Model( grid_width: initial_grid_width, grid_height: initial_grid_height, - formula_mode: False, + display_mode: DisplayValues, active_cell: None, src_grid:, value_grid:, @@ -63,11 +74,11 @@ fn init(_flags) -> #(Model, effect.Effect(Msg)) { type Msg { Noop - UserToggledFormulaMode(to: Bool) + UserToggledDisplayMode(to: DisplayMode) UserSetCellValue(key: grid.GridKey, val: String) UserFocusedOnCell(key: grid.GridKey) UserFocusedOffCell - UserResizedGrid(new_width: Int, new_height: Int) + UserHitKeyInCell(key: grid.GridKey, keyboard_key: String) } fn update_grid(model: Model) -> Model { @@ -96,32 +107,29 @@ fn update(model: Model, msg: Msg) -> #(Model, effect.Effect(Msg)) { Model(..model, src_grid: grid.insert(model.src_grid, key, val)) #(update_grid(model), effect.none()) } - UserToggledFormulaMode(formula_mode) -> #( - Model(..model, formula_mode:), - effect.none(), - ) + UserToggledDisplayMode(display_mode) -> { + #(Model(..model, display_mode:), effect.none()) + } UserFocusedOnCell(key) -> { #(Model(..model, active_cell: Some(key)), effect.none()) } UserFocusedOffCell -> { #(Model(..model, active_cell: None), effect.none()) } - UserResizedGrid(new_width, new_height) -> { - #( - Model( - ..model, - grid_width: new_width, - grid_height: new_height, - src_grid: grid.resize(model.src_grid, new_width, new_height, ""), - value_grid: grid.resize( - model.value_grid, - new_width, - new_height, - Ok(value.Empty), - ), - ), - effect.none(), - ) + UserHitKeyInCell(key, keyboard_key) -> { + case keyboard_key { + "Enter" -> { + let new_active_cell = grid.cell_underneath(model.src_grid, key) + case new_active_cell { + Error(_) -> #(Model(..model, active_cell: None), effect.none()) + Ok(new) -> { + focus(grid.to_string(new)) + #(Model(..model, active_cell: Some(new)), effect.none()) + } + } + } + _ -> #(model, effect.none()) + } } } } @@ -136,51 +144,28 @@ fn view(model: Model) -> element.Element(Msg) { }) |> result.unwrap(or: html.div([], [])) - let resize_menu = - html.div([], [ - html.label([], t("Width")), - html.input([ - event.on_input(fn(txt) { - case int.parse(txt) { - Error(_) -> Noop - Ok(n) -> UserResizedGrid(n, model.grid_height) - } - }), - attribute.value(model.grid_width |> int.to_string), - ]), - html.label([], t("Height")), - html.input([ - event.on_input(fn(txt) { - case int.parse(txt) { - Error(_) -> Noop - Ok(n) -> UserResizedGrid(model.grid_width, n) - } - }), - attribute.value(model.grid_height |> int.to_string), - ]), - ]) - let rows = model.src_grid.cells |> list.group(grid.row) |> dict.map_values(fn(_, keys) { let cells = list.map(keys, fn(key) { + let on_enter = event.on_keydown(UserHitKeyInCell(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)) - let show_formula = - model.active_cell == Some(key) || model.formula_mode - let value = - case show_formula { - True -> grid.get(model.src_grid, key) - False -> - case grid.get(model.value_grid, key) { + let id = attribute.id(grid.to_string(key)) + let value = case model.display_mode { + DisplayFormulas -> grid.get(model.src_grid, key) + DisplayGridCoords -> string.inspect(key) + DisplayValues -> case model.active_cell == Some(key) { + False -> case grid.get(model.value_grid, key) { Error(e) -> error.error_type_string(e) Ok(v) -> value.value_to_string(v) } + True -> grid.get(model.src_grid, key) } - |> attribute.value + } |> attribute.value let styles = case list.find(model.errors_to_display, fn(i) { i.0 == key }) @@ -189,34 +174,73 @@ fn view(model: Model) -> element.Element(Msg) { Ok(_) -> [attribute.style([#("background-color", "red")])] } + let input = html.input([ + on_input, + on_focus, + out_of_focus, + value, + on_enter, + id + ]) + html.td(styles, { - [html.input([on_input, on_focus, out_of_focus, value])] + [ + input + ] }) }) html.tr([], cells) }) - |> dict.values + |> dict.to_list + |> list.sort(fn(r1, r2) { int.compare(r1.0, r2.0) }) + |> list.map(fn(e) { e.1 }) let grid = html.div([class("table-container")], [ - html.table([], [html.tbody([], rows)]), + html.table([attribute.class("tg")], [html.tbody([], rows)]), ]) let formula_mode_toggle = html.input([ - attribute.type_("checkbox"), + attribute.type_("radio"), + attribute.name("display_mode"), attribute.id("formula_mode"), - event.on_check(UserToggledFormulaMode), + event.on_check(fn(_) { UserToggledDisplayMode(DisplayFormulas) }), ]) let formula_mode_toggle_label = - html.label([attribute.for("formula_mode")], t("toggle formula mode")) + html.label([attribute.for("formula_mode")], t("Show formulas")) + + let grid_mode_toggle = + html.input([ + attribute.type_("radio"), + attribute.name("display_mode"), + attribute.id("grid_mode"), + event.on_check(fn(_) { UserToggledDisplayMode(DisplayGridCoords) }), + ]) + + let grid_mode_toggle_label = + html.label([attribute.for("grid_mode")], t("Show grid coordinates")) + + let value_mode_toggle = + html.input([ + attribute.type_("radio"), + attribute.name("display_mode"), + attribute.id("value_mode"), + event.on_check(fn(_) { UserToggledDisplayMode(DisplayValues) }), + ]) + + let value_mode_toggle_label = + html.label([attribute.for("value_mode")], t("Show evaluated values")) html.div([], [ + value_mode_toggle, + value_mode_toggle_label, formula_mode_toggle, formula_mode_toggle_label, - resize_menu, + grid_mode_toggle, + grid_mode_toggle_label, grid, error_to_display, ]) @@ -226,7 +250,7 @@ fn error_view(re: renderable_error.RenderableError) { html.div([], [ html.h4([], t(re.title)), html.p([], t(re.info)), - .. case re.hint { + ..case re.hint { None -> [] Some(hint) -> [html.p([], t(hint))] } diff --git a/squared_away/src/squared_away_ffi.js b/squared_away/src/squared_away_ffi.js new file mode 100644 index 0000000..b5e84f7 --- /dev/null +++ b/squared_away/src/squared_away_ffi.js @@ -0,0 +1,3 @@ +export function focus(id) { + document.getElementById(id).focus() +} \ No newline at end of file diff --git a/squared_away_lang/src/renderable_error.gleam b/squared_away_lang/src/renderable_error.gleam index fae39a9..2f34974 100644 --- a/squared_away_lang/src/renderable_error.gleam +++ b/squared_away_lang/src/renderable_error.gleam @@ -6,9 +6,5 @@ import gleam/option.{type Option} /// me as just one person. /// It will hopefully also help to keep error's looking consistent in the UI. pub type RenderableError { - RenderableError( - title: String, - info: String, - hint: Option(String) - ) -} \ No newline at end of file + RenderableError(title: String, info: String, hint: Option(String)) +} diff --git a/squared_away_lang/src/squared_away_lang/error.gleam b/squared_away_lang/src/squared_away_lang/error.gleam index 4ec57db..577838a 100644 --- a/squared_away_lang/src/squared_away_lang/error.gleam +++ b/squared_away_lang/src/squared_away_lang/error.gleam @@ -1,9 +1,9 @@ +import gleam/option.{None} import renderable_error import squared_away_lang/interpreter/runtime_error import squared_away_lang/parser/parse_error import squared_away_lang/scanner/scan_error import squared_away_lang/typechecker/type_error -import gleam/option.{None} pub type CompileError { ScanError(scan_error.ScanError) @@ -15,7 +15,14 @@ pub type CompileError { pub fn to_renderable_error(ce: CompileError) -> renderable_error.RenderableError { case ce { TypeError(te) -> type_error.to_renderable_error(te) - _ -> renderable_error.RenderableError(title: "Compiler error", info: "Todo: implement this error description", hint: None) + ParseError(pe) -> + renderable_error.RenderableError(title: "", info: pe.context, hint: None) + _ -> + renderable_error.RenderableError( + title: "Compiler error", + info: "Todo: implement this error description", + hint: None, + ) } } diff --git a/squared_away_lang/src/squared_away_lang/grid.gleam b/squared_away_lang/src/squared_away_lang/grid.gleam index 78b678e..3a30047 100644 --- a/squared_away_lang/src/squared_away_lang/grid.gleam +++ b/squared_away_lang/src/squared_away_lang/grid.gleam @@ -2,6 +2,7 @@ //// boilerplate around results for get operations, so I'm gonna try and extract //// them to a module +import gleam/int import gleam/dict import gleam/list import gleam/result @@ -16,6 +17,11 @@ pub opaque type GridKey { GridKey(row: Int, col: Int) } +pub fn to_string(key: GridKey) -> String { + let GridKey(row, col) = key + int.to_string(row) <> "_" <> int.to_string(col) +} + pub fn row(grid_key: GridKey) -> Int { grid_key.row } @@ -70,6 +76,10 @@ 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 cell_underneath(grid: Grid(a), key: GridKey) -> Result(GridKey, Nil) { + list.find(grid.cells, fn(k) { k.row == key.row + 1 && k.col == key.col }) +} + 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 @@ -82,26 +92,3 @@ pub fn intersect(row_cell: GridKey, col_cell: GridKey) -> Result(GridKey, Nil) { pub fn to_list(grid: Grid(a)) -> List(#(GridKey, a)) { dict.to_list(grid.inner) } - -pub fn resize(old_grid: Grid(a), 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 - let key = GridKey(row:, col:) - let val = dict.get(old_grid.inner, key) |> result.unwrap(or: default) - - #(dict.insert(g, key, val), [key, ..c]) - }) - - #(dict.merge(grid, g), list.concat([c, cells])) - }) - - Grid(g, c) -} diff --git a/squared_away_lang/src/squared_away_lang/parser.gleam b/squared_away_lang/src/squared_away_lang/parser.gleam index 9da3cb3..c75dde8 100644 --- a/squared_away_lang/src/squared_away_lang/parser.gleam +++ b/squared_away_lang/src/squared_away_lang/parser.gleam @@ -1,3 +1,4 @@ +import gleam/bool import gleam/io import gleam/result import gleam/string @@ -102,54 +103,132 @@ fn try_parse_binary_ops( case tokens { [token.Plus, ..rest] -> { use #(rhs, rest) <- result.try(do_parse(rest)) + use <- bool.guard( + rhs == expr.Empty, + Error(parse_error.ParseError( + "No item on right hand side of binary operation.", + )), + ) Ok(#(expr.BinaryOp(_, expr.Add, rhs), rest)) } [token.Minus, ..rest] -> { use #(rhs, rest) <- result.try(do_parse(rest)) + use <- bool.guard( + rhs == expr.Empty, + Error(parse_error.ParseError( + "No item on right hand side of binary operation.", + )), + ) Ok(#(expr.BinaryOp(_, expr.Subtract, rhs), rest)) } [token.Star, ..rest] -> { use #(rhs, rest) <- result.try(do_parse(rest)) + use <- bool.guard( + rhs == expr.Empty, + Error(parse_error.ParseError( + "No item on right hand side of binary operation.", + )), + ) Ok(#(expr.BinaryOp(_, expr.Multiply, rhs), rest)) } [token.Div, ..rest] -> { use #(rhs, rest) <- result.try(do_parse(rest)) + use <- bool.guard( + rhs == expr.Empty, + Error(parse_error.ParseError( + "No item on right hand side of binary operation.", + )), + ) Ok(#(expr.BinaryOp(_, expr.Divide, rhs), rest)) } [token.StarStar, ..rest] -> { use #(rhs, rest) <- result.try(do_parse(rest)) + use <- bool.guard( + rhs == expr.Empty, + Error(parse_error.ParseError( + "No item on right hand side of binary operation.", + )), + ) Ok(#(expr.BinaryOp(_, expr.Power, rhs), rest)) } [token.BangEqual, ..rest] -> { use #(rhs, rest) <- result.try(do_parse(rest)) + use <- bool.guard( + rhs == expr.Empty, + Error(parse_error.ParseError( + "No item on right hand side of binary operation.", + )), + ) Ok(#(expr.BinaryOp(_, expr.NotEqualCheck, rhs), rest)) } [token.EqualEqual, ..rest] -> { use #(rhs, rest) <- result.try(do_parse(rest)) + use <- bool.guard( + rhs == expr.Empty, + Error(parse_error.ParseError( + "No item on right hand side of binary operation.", + )), + ) Ok(#(expr.BinaryOp(_, expr.EqualCheck, rhs), rest)) } [token.LessEqual, ..rest] -> { use #(rhs, rest) <- result.try(do_parse(rest)) + use <- bool.guard( + rhs == expr.Empty, + Error(parse_error.ParseError( + "No item on right hand side of binary operation.", + )), + ) Ok(#(expr.BinaryOp(_, expr.LessThanOrEqualCheck, rhs), rest)) } [token.Less, ..rest] -> { use #(rhs, rest) <- result.try(do_parse(rest)) + use <- bool.guard( + rhs == expr.Empty, + Error(parse_error.ParseError( + "No item on right hand side of binary operation.", + )), + ) Ok(#(expr.BinaryOp(_, expr.LessThanCheck, rhs), rest)) } [token.GreaterEqual, ..rest] -> { use #(rhs, rest) <- result.try(do_parse(rest)) + use <- bool.guard( + rhs == expr.Empty, + Error(parse_error.ParseError( + "No item on right hand side of binary operation.", + )), + ) Ok(#(expr.BinaryOp(_, expr.GreaterThanOrEqualCheck, rhs), rest)) } [token.Greater, ..rest] -> { use #(rhs, rest) <- result.try(do_parse(rest)) + use <- bool.guard( + rhs == expr.Empty, + Error(parse_error.ParseError( + "No item on right hand side of binary operation.", + )), + ) Ok(#(expr.BinaryOp(_, expr.GreaterThanCheck, rhs), rest)) } [token.And, ..rest] -> { use #(rhs, rest) <- result.try(do_parse(rest)) + use <- bool.guard( + rhs == expr.Empty, + Error(parse_error.ParseError( + "No item on right hand side of binary operation.", + )), + ) Ok(#(expr.BinaryOp(_, expr.And, rhs), rest)) } [token.Or, ..rest] -> { use #(rhs, rest) <- result.try(do_parse(rest)) + use <- bool.guard( + rhs == expr.Empty, + Error(parse_error.ParseError( + "No item on right hand side of binary operation.", + )), + ) Ok(#(expr.BinaryOp(_, expr.Or, rhs), rest)) } _ -> Error(parse_error.ParseError("Not a binary operation")) diff --git a/squared_away_lang/src/squared_away_lang/typechecker/type_error.gleam b/squared_away_lang/src/squared_away_lang/typechecker/type_error.gleam index d4bf706..1f053f2 100644 --- a/squared_away_lang/src/squared_away_lang/typechecker/type_error.gleam +++ b/squared_away_lang/src/squared_away_lang/typechecker/type_error.gleam @@ -1,7 +1,7 @@ +import gleam/option.{None} import renderable_error import squared_away_lang/parser/expr import squared_away_lang/typechecker/typ -import gleam/option.{None} pub type TypeError { TypeError(context: String) @@ -14,11 +14,23 @@ pub type TypeError { pub fn to_renderable_error(te: TypeError) -> renderable_error.RenderableError { case te { - IncorrectTypesForBinaryOp(lhs, rhs, op) -> renderable_error.RenderableError( - title: "Unexpected arguments to binary operation `&&`", - info: "Expected booleans. Got " <> typ.to_string(lhs) <> " on the left and " <> typ.to_string(rhs) <> " on the right", - hint: None) - TypeError(_) -> renderable_error.RenderableError(title: "Type Error", info: "Todo: Fill in this error", hint: None) + IncorrectTypesForBinaryOp(lhs, rhs, op) -> + renderable_error.RenderableError( + title: "Unexpected arguments to binary operation " + <> describe_binary_op_kind_for_err(op), + info: "Expected booleans. Got " + <> typ.to_string(lhs) + <> " on the left and " + <> typ.to_string(rhs) + <> " on the right", + hint: None, + ) + TypeError(_) -> + renderable_error.RenderableError( + title: "Type Error", + info: "Todo: Fill in this error", + hint: None, + ) } }