Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
381 changes: 107 additions & 274 deletions exercises/practice/sgf-parsing/.meta/Example.roc
Original file line number Diff line number Diff line change
@@ -1,130 +1,6 @@
# import parser.Parser as P
# import parser.String as S
#
# SgfParsing :: {}.{
# NodeProperties : Dict(Str, List(Str))
# GameTree := { properties : NodeProperties, children : List(GameTree) }.{
# # The following line enables the default `is_eq` implementation
# is_eq : _
# }
#
# parse : Str -> Try(GameTree, [ParsingFailure(Str), ParsingIncomplete(Str)])
# parse = |sgf| {
# S.parse_str(game_tree, sgf)
# }
# }
#
## --- SGF GRAMMAR ---
## Source: https://homepages.cwi.nl/~aeb/go/misc/sgfnotes.html
##
## Collection = GameTree+
## GameTree = '(' Sequence GameTree* ')'
## Sequence = Node+
## Node = ';' Property*
## Property = PropIdent PropValue+
## PropIdent = UcLetter+
## PropValue = '[' CValueType ']'
## CValueType = (ValueType | Compose)
## ValueType = (None | Number | Real | Double | Color | SimpleText | Text | Point | Move | Stone)
## UcLetter = 'A'..'Z'
## Compose = ValueType ':' ValueType
##
## Note: In this exercise, we only support `Text` values for `CValueType`, where
## `Text` is escaped as per the instructions. `Compose` is not supported.
#
# build_game_node : List(SgfParsing.NodeProperties), List(SgfParsing.GameTree) -> SgfParsing.GameTree
# build_game_node = |node_props, alternatives| {
# help : List(SgfParsing.NodeProperties), List(SgfParsing.GameTree) -> SgfParsing.GameTree
# help = |remaining_node_props, sub_trees| {
# match remaining_node_props {
# [root_node] =>
# { properties: root_node, children: sub_trees }
#
# [.. as rest, last] =>
# help(rest, [{ properties: last, children: sub_trees }])
#
# [] => {
# crash "Unreachable: remaining_node_props list cannot be empty"
# }
# }
# }
# help(node_props, alternatives)
# }
#
# game_tree : P.Parser(List(U8), SgfParsing.GameTree)
# game_tree =
# P.const(|node_props| |alternatives| build_game_node(node_props, alternatives))
# .skip(S.codeunit('('))
# .keep(node.one_or_more())
# .keep(sub_tree.many())
# .skip(S.codeunit(')'))
#
# sub_tree : P.Parser(List(U8), SgfParsing.GameTree)
# sub_tree =
# P.const(|t| t)
# .keep(
# P.const(|t| t).keep(P.lazy(|_| game_tree)),
# )
#
# node : P.Parser(List(U8), SgfParsing.NodeProperties)
# node =
# P.const(|s| s)
# .skip(S.codeunit(';'))
# .keep(P.many(property))
# .map(|properties| Dict.from_list(properties))
#
# property : P.Parser(List(U8), (Str, List(Str)))
# property =
# P.map2(
# prop_ident,
# P.one_or_more(prop_value),
# |id, values|
# (
# ((id |> Str.from_utf8) ?? "<BadUTF8>"),
# values.map(|value| ((value |> Str.from_utf8) ?? "<BadUTF8>")),
# ),
# )
#
# prop_ident : P.Parser(List(U8), List(U8))
# prop_ident =
# P.one_or_more(uc_letter)
#
# prop_value : P.Parser(List(U8), List(U8))
# prop_value =
# P.const(|value| value)
# .skip(S.codeunit('['))
# .keep(value_type) # in this exercise we don't support 'Compose'
# .skip(S.codeunit(']'))
#
# value_type : P.Parser(List(U8), List(U8))
# value_type =
# P.build_primitive_parser(
# |input| {
# help = |result, chars| {
# match chars {
# [] => Err(ParsingFailure("No closing bracket"))
# [']', ..] => Ok({ val: result, input: chars })
# ['\\', '\t', .. as rest] => help((result.append(' ')), rest)
# ['\\', '\n', .. as rest] => help(result, rest)
# ['\\', c, .. as rest] => help((result.append(c)), rest)
# ['\t', .. as rest] => help((result.append(' ')), rest)
# [c, .. as rest] => help((result.append(c)), rest)
# }
# }
# help([], input)
# },
# )
#
# uc_letter : P.Parser(List(U8), U8)
# uc_letter =
# S.codeunit_satisfies(|b| b >= 'A' and b <= 'Z')
import parser.Parser as P
import parser.String as S

# WARNING: The correct solution is above, but it currently triggers issue
# https://github.com/roc-lang/roc/issues/10303
# so I've temporarily hard-coded all the tested
# input/output pairs, so the tests can pass, and users can try solving
# this exercise (hopefully without running into the same issue).
# I will revert to the actual solution once the compiler issue is fixed.
SgfParsing :: {}.{
NodeProperties : Dict(Str, List(Str))
GameTree := { properties : NodeProperties, children : List(GameTree) }.{
Expand All @@ -134,154 +10,111 @@ SgfParsing :: {}.{

parse : Str -> Try(GameTree, [ParsingFailure(Str), ParsingIncomplete(Str)])
parse = |sgf| {
match sgf {
"" => Err(ParsingFailure("Empty input"))
"()" => Err(ParsingFailure("Tree with no nodes"))
";" => Err(ParsingFailure("Node without tree"))
"(;)" => Ok({ properties: Dict.empty(), children: [] })
"(;A[B])" => Ok({
properties: Dict.from_list([("A", ["B"])]),
children: [],
})
"(;A)" => Err(ParsingFailure("Properties without delimiter"))
"(;a[b])" => Err(ParsingFailure("All lowercase property"))
"(;Aa[b])" => Err(ParsingFailure("Upper and lowercase property"))
"(;A[b]C[d])" => Ok({
properties: Dict.from_list([
("A", ["b"]),
("C", ["d"]),
]),
children: [],
})
"(;A[B];B[C])" => Ok({
properties: Dict.from_list([
("A", ["B"]),
]),
children: [
{
properties: Dict.from_list([
("B", ["C"]),
]),
children: [],
},
],
})
"(;A[B](;B[C])(;C[D]))" => Ok({
properties: Dict.from_list([
("A", ["B"]),
]),
children: [
{
properties: Dict.from_list([
("B", ["C"]),
]),
children: [],
},
{
properties: Dict.from_list([
("C", ["D"]),
]),
children: [],
},
],
})
"(;A[b][c][d])" => Ok({
properties: Dict.from_list([
("A", ["b", "c", "d"]),
]),
children: [],
})
"(;A[hello\t\tworld])" => Ok({
properties: Dict.from_list([
("A", ["hello world"]),
]),
children: [],
})
"(;A[hello\n\nworld])" => Ok({
properties: Dict.from_list([
("A", ["hello\n\nworld"]),
]),
children: [],
})
"(;A[\\]])" => Ok({
properties: Dict.from_list([
("A", ["]"]),
]),
children: [],
})
"(;A[\\\\])" => Ok({
properties: Dict.from_list([
("A", ["\\"]),
]),
children: [],
})
"(;A[x[y\\]z][foo]B[bar];C[baz])" => Ok({
properties: Dict.from_list([
("A", ["x[y]z", "foo"]),
("B", ["bar"]),
]),
children: [
{
properties: Dict.from_list([
("C", ["baz"]),
]),
children: [],
},
],
})
"(;A[a;b][foo]B[bar];C[baz])" => Ok({
properties: Dict.from_list([
("A", ["a;b", "foo"]),
("B", ["bar"]),
]),
children: [
{
properties: Dict.from_list([
("C", ["baz"]),
]),
children: [],
},
],
})
"(;A[x(y)z][foo]B[bar];C[baz])" => Ok({
properties: Dict.from_list([
("A", ["x(y)z", "foo"]),
("B", ["bar"]),
]),
children: [
{
properties: Dict.from_list([
("C", ["baz"]),
]),
children: [],
},
],
})
"(;A[hello\\\tworld])" => Ok({
properties: Dict.from_list([
("A", ["hello world"]),
]),
children: [],
})
"(;A[hello\\\nworld])" => Ok({
properties: Dict.from_list([
("A", ["helloworld"]),
]),
children: [],
})
"(;A[\\t = t and \\n = n])" => Ok({
properties: Dict.from_list([
("A", ["t = t and n = n"]),
]),
children: [],
})
"(;A[\\]b\nc\\\nd\t\te\\\\ \\\n\\]])" => Ok({
properties: Dict.from_list([
("A", ["]b\ncd e\\ ]"]),
]),
children: [],
})
_ => Err(ParsingFailure("Unsupported input"))
S.parse_str(game_tree, sgf)
}
}

# --- SGF GRAMMAR ---
# Source: https://homepages.cwi.nl/~aeb/go/misc/sgfnotes.html
#
# Collection = GameTree+
# GameTree = '(' Sequence GameTree* ')'
# Sequence = Node+
# Node = ';' Property*
# Property = PropIdent PropValue+
# PropIdent = UcLetter+
# PropValue = '[' CValueType ']'
# CValueType = (ValueType | Compose)
# ValueType = (None | Number | Real | Double | Color | SimpleText | Text | Point | Move | Stone)
# UcLetter = 'A'..'Z'
# Compose = ValueType ':' ValueType
#
# Note: In this exercise, we only support `Text` values for `CValueType`, where
# `Text` is escaped as per the instructions. `Compose` is not supported.

build_game_node : List(SgfParsing.NodeProperties), List(SgfParsing.GameTree) -> SgfParsing.GameTree
build_game_node = |node_props, alternatives| {
help : List(SgfParsing.NodeProperties), List(SgfParsing.GameTree) -> SgfParsing.GameTree
help = |remaining_node_props, sub_trees| {
match remaining_node_props {
[root_node] =>
{ properties: root_node, children: sub_trees }

[.. as rest, last] =>
help(rest, [{ properties: last, children: sub_trees }])

[] => {
crash "Unreachable: remaining_node_props list cannot be empty"
}
}
}
help(node_props, alternatives)
}

game_tree : P.Parser(List(U8), SgfParsing.GameTree)
game_tree =
P.const(|node_props| |alternatives| build_game_node(node_props, alternatives))
.skip(S.codeunit('('))
.keep(node.one_or_more())
.keep(sub_tree.many())
.skip(S.codeunit(')'))

sub_tree : P.Parser(List(U8), SgfParsing.GameTree)
sub_tree =
P.const(|t| t)
.keep(
P.const(|t| t).keep(P.lazy(|_| game_tree)),
)

node : P.Parser(List(U8), SgfParsing.NodeProperties)
node =
P.const(|s| s)
.skip(S.codeunit(';'))
.keep(P.many(property))
.map(|properties| Dict.from_list(properties))

property : P.Parser(List(U8), (Str, List(Str)))
property =
P.map2(
prop_ident,
P.one_or_more(prop_value),
|id, values|
(
((id |> Str.from_utf8) ?? "<BadUTF8>"),
values.map(|value| ((value |> Str.from_utf8) ?? "<BadUTF8>")),
),
)

prop_ident : P.Parser(List(U8), List(U8))
prop_ident =
P.one_or_more(uc_letter)

prop_value : P.Parser(List(U8), List(U8))
prop_value =
P.const(|value| value)
.skip(S.codeunit('['))
.keep(value_type) # in this exercise we don't support 'Compose'
.skip(S.codeunit(']'))

value_type : P.Parser(List(U8), List(U8))
value_type =
P.build_primitive_parser(
|input| {
help = |result, chars| {
match chars {
[] => Err(ParsingFailure("No closing bracket"))
[']', ..] => Ok({ val: result, input: chars })
['\\', '\t', .. as rest] => help((result.append(' ')), rest)
['\\', '\n', .. as rest] => help(result, rest)
['\\', c, .. as rest] => help((result.append(c)), rest)
['\t', .. as rest] => help((result.append(' ')), rest)
[c, .. as rest] => help((result.append(c)), rest)
}
}
help([], input)
},
)

uc_letter : P.Parser(List(U8), U8)
uc_letter =
S.codeunit_satisfies(|b| b >= 'A' and b <= 'Z')
Loading