From ad62137a0c77531c903592f3bfa3deb3e304abc9 Mon Sep 17 00:00:00 2001 From: Tonie Victor Date: Sun, 1 Mar 2026 13:34:48 +0100 Subject: [PATCH 1/3] wip --- gleam.toml | 1 + manifest.toml | 2 + src/rexen.gleam | 5 ++ src/rexen/nfa/machine.gleam | 84 ++++++++++++++++++--------- test/grammar_test.gleam | 110 ------------------------------------ test/rexen_test.gleam | 32 ----------- 6 files changed, 65 insertions(+), 169 deletions(-) delete mode 100644 test/grammar_test.gleam delete mode 100644 test/rexen_test.gleam diff --git a/gleam.toml b/gleam.toml index cf63509..f6d0347 100644 --- a/gleam.toml +++ b/gleam.toml @@ -14,6 +14,7 @@ repository = { type = "github", user = "tonievictor", repo = "rexen" } [dependencies] gleam_stdlib = ">= 0.34.0 and < 2.0.0" +gleam_erlang = ">= 1.3.0 and < 2.0.0" [dev-dependencies] gleeunit = ">= 1.0.0 and < 2.0.0" diff --git a/manifest.toml b/manifest.toml index 9b16056..4e3940b 100644 --- a/manifest.toml +++ b/manifest.toml @@ -2,10 +2,12 @@ # You typically do not need to edit this file packages = [ + { name = "gleam_erlang", version = "1.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_erlang", source = "hex", outer_checksum = "1124AD3AA21143E5AF0FC5CF3D9529F6DB8CA03E43A55711B60B6B7B3874375C" }, { name = "gleam_stdlib", version = "0.54.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "723BA61A2BAE8D67406E59DD88CEA1B3C3F266FC8D70F64BE9FEC81B4505B927" }, { name = "gleeunit", version = "1.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "0E6C83834BA65EDCAAF4FE4FB94AC697D9262D83E6F58A750D63C9F6C8A9D9FF" }, ] [requirements] +gleam_erlang = { version = ">= 1.3.0 and < 2.0.0" } gleam_stdlib = { version = ">= 0.34.0 and < 2.0.0" } gleeunit = { version = ">= 1.0.0 and < 2.0.0" } diff --git a/src/rexen.gleam b/src/rexen.gleam index 7c8af3d..af05e2c 100644 --- a/src/rexen.gleam +++ b/src/rexen.gleam @@ -47,3 +47,8 @@ pub fn new(expression: String) -> Result(machine.NFA, String) { pub fn compute(engine: machine.NFA, input: String) -> Bool { machine.evaluate(engine, input) } + +pub fn main() { + let assert Ok(nfa) = new("(a*)*") + echo compute(nfa, "b") +} diff --git a/src/rexen/nfa/machine.gleam b/src/rexen/nfa/machine.gleam index d529130..eafff10 100644 --- a/src/rexen/nfa/machine.gleam +++ b/src/rexen/nfa/machine.gleam @@ -1,5 +1,6 @@ import gleam/dict import gleam/list +import gleam/set.{type Set} import gleam/string import rexen/nfa/state @@ -65,42 +66,63 @@ pub fn add_transition( ) } -pub type StackValue { - StackValue(i: Int, state: state.State) +pub type Node { + Node(index: Int, state: state.State) } pub fn evaluate(nfa: NFA, input: String) -> Bool { let assert Ok(s) = dict.get(nfa.states, nfa.initial_state) - let stack = [StackValue(0, s)] - evaluate_loop(nfa, input, stack) + + let nodes = [Node(0, s)] + let visited_nodes: Set(Node) = set.new() + + evaluate_loop(nfa, input, nodes, visited_nodes) } -fn evaluate_loop(nfa: NFA, input: String, stack: List(StackValue)) -> Bool { - case stack { +fn evaluate_loop( + nfa: NFA, + input: String, + nodes: List(Node), + visited_nodes: Set(Node), +) -> Bool { + case nodes { [] -> False - [value, ..rest] -> { - case list.contains(nfa.ending_states, value.state.name) { + [node, ..rest] -> { + let visited_nodes = + set.insert(visited_nodes, Node(index: node.index, state: node.state)) + + case list.contains(nfa.ending_states, node.state.name) { True -> - case string.length(input) == value.i { + case string.length(input) == node.index { True -> True False -> { - let char = string.slice(input, value.i, 1) - let new_stack = + let char = string.slice(input, node.index, 1) + let updated_nodes = process_transitions( nfa, - value.state.transitions, + node.state.transitions, rest, - value, + visited_nodes, + node, char, ) - evaluate_loop(nfa, input, new_stack) + + evaluate_loop(nfa, input, updated_nodes, visited_nodes) } } False -> { - let char = string.slice(input, value.i, 1) - let new_stack = - process_transitions(nfa, value.state.transitions, rest, value, char) - evaluate_loop(nfa, input, new_stack) + let char = string.slice(input, node.index, 1) + let updated_nodes = + process_transitions( + nfa, + node.state.transitions, + rest, + visited_nodes, + node, + char, + ) + + evaluate_loop(nfa, input, updated_nodes, visited_nodes) } } } @@ -110,25 +132,33 @@ fn evaluate_loop(nfa: NFA, input: String, stack: List(StackValue)) -> Bool { fn process_transitions( nfa: NFA, transitions: List(state.Transition), - stack: List(StackValue), - stkv: StackValue, + nodes: List(Node), + visited_nodes: Set(Node), + node: Node, char: String, -) -> List(StackValue) { +) -> List(Node) { case transitions { - [] -> stack + [] -> nodes [#(matcher, name), ..rest] -> { case state.matches(matcher, char) { False -> { - process_transitions(nfa, rest, stack, stkv, char) + process_transitions(nfa, rest, nodes, visited_nodes, node, char) } True -> { let index = case state.is_epsilon(matcher) { - True -> stkv.i - False -> stkv.i + 1 + True -> node.index + False -> node.index + 1 } + let assert Ok(to) = dict.get(nfa.states, name) - let new_stack = list.append(stack, [StackValue(i: index, state: to)]) - process_transitions(nfa, rest, new_stack, stkv, char) + let new_node = Node(index: index, state: to) + + let nodes = case set.contains(visited_nodes, new_node) { + True -> nodes + False -> list.append(nodes, [new_node]) + } + + process_transitions(nfa, rest, nodes, visited_nodes, node, char) } } } diff --git a/test/grammar_test.gleam b/test/grammar_test.gleam deleted file mode 100644 index 48255b5..0000000 --- a/test/grammar_test.gleam +++ /dev/null @@ -1,110 +0,0 @@ -import gleam/list -import gleeunit/should -import rexen/grammar.{ - type Token, Asterix, Bar, CParen, Dot, Letter, OParen, Operator, QMark, -} - -pub fn to_string_test() { - let tokens = [ - [ - Operator(OParen), - Letter("a"), - Operator(Dot(2)), - Letter("b"), - CParen, - Operator(Asterix(3)), - ], - [ - Letter("a"), - Operator(Asterix(3)), - Letter("b"), - Operator(Asterix(3)), - Operator(Dot(2)), - Letter("c"), - Operator(Dot(2)), - Letter("d"), - Letter("e"), - Operator(Bar(1)), - Operator(Dot(2)), - ], - [ - Operator(OParen), - Letter("a"), - Operator(Dot(2)), - Letter("b"), - CParen, - Operator(QMark(3)), - ], - ] - to_string_loop(tokens, []) - |> should.equal(["(ab)*", "a*b*cde|", "(ab)?"]) -} - -fn to_string_loop( - tokens: List(List(Token)), - output: List(String), -) -> List(String) { - case tokens { - [] -> output - [toks, ..rest] -> { - to_string_loop(rest, list.append(output, [grammar.to_string(toks, "")])) - } - } -} - -pub fn shunt_ok_test() { - let input = ["(a*b*)c(d|e)", "a|b", "a*b"] - shunt_ok_loop(input, []) - |> should.equal([ - [ - Letter("a"), - Operator(Asterix(3)), - Letter("b"), - Operator(Asterix(3)), - Operator(Dot(2)), - Letter("c"), - Operator(Dot(2)), - Letter("d"), - Letter("e"), - Operator(Bar(1)), - Operator(Dot(2)), - ], - [Letter("a"), Letter("b"), Operator(Bar(1))], - [Letter("a"), Operator(Asterix(3)), Letter("b"), Operator(Dot(2))], - ]) -} - -fn shunt_ok_loop( - input: List(String), - output: List(List(Token)), -) -> List(List(Token)) { - case input { - [] -> output - [str, ..rest] -> { - let assert Ok(out) = grammar.shunt(str) - shunt_ok_loop(rest, list.append(output, [out])) - } - } -} - -// the shunt function returns an error only when a closing bracket does not -// have a corresponding opening bracket. -pub fn shunt_test() { - let input = ["(a*|)", "abab|)"] - shunt_loop(input, []) - |> should.equal([True, False]) -} - -// This append False to the output list when we have an error and True on okay -fn shunt_loop(input: List(String), output: List(Bool)) -> List(Bool) { - case input { - [] -> output - [str, ..rest] -> { - let out = case grammar.shunt(str) { - Ok(_) -> True - Error(_) -> False - } - shunt_loop(rest, list.append(output, [out])) - } - } -} diff --git a/test/rexen_test.gleam b/test/rexen_test.gleam deleted file mode 100644 index d63f7ec..0000000 --- a/test/rexen_test.gleam +++ /dev/null @@ -1,32 +0,0 @@ -import gleam/list -import gleeunit -import gleeunit/should -import rexen -import rexen/nfa/machine - -pub fn main() { - gleeunit.main() -} - -pub fn compute_test() { - let assert Ok(nfa) = rexen.new("(a*b*)c(d|e)") - compute_loop( - nfa, - ["abcd", "abce", "aabbbcd", "aaaabbbbce", "bc", "bceeee"], - [], - ) - |> should.equal([True, True, True, True, False, False]) -} - -fn compute_loop( - nfa: machine.NFA, - input: List(String), - output: List(Bool), -) -> List(Bool) { - case input { - [] -> output - [str, ..rest] -> { - compute_loop(nfa, rest, list.append(output, [rexen.compute(nfa, str)])) - } - } -} From 653b100c809e07fe51e9e3a015ecc428a3f6bf7c Mon Sep 17 00:00:00 2001 From: Tonie Victor Date: Sun, 1 Mar 2026 13:56:16 +0100 Subject: [PATCH 2/3] cleanup --- gleam.toml | 1 - manifest.toml | 2 -- src/rexen.gleam | 5 ----- src/rexen/nfa/machine.gleam | 40 +++++++++++++++++++++---------------- 4 files changed, 23 insertions(+), 25 deletions(-) diff --git a/gleam.toml b/gleam.toml index f6d0347..cf63509 100644 --- a/gleam.toml +++ b/gleam.toml @@ -14,7 +14,6 @@ repository = { type = "github", user = "tonievictor", repo = "rexen" } [dependencies] gleam_stdlib = ">= 0.34.0 and < 2.0.0" -gleam_erlang = ">= 1.3.0 and < 2.0.0" [dev-dependencies] gleeunit = ">= 1.0.0 and < 2.0.0" diff --git a/manifest.toml b/manifest.toml index 4e3940b..9b16056 100644 --- a/manifest.toml +++ b/manifest.toml @@ -2,12 +2,10 @@ # You typically do not need to edit this file packages = [ - { name = "gleam_erlang", version = "1.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_erlang", source = "hex", outer_checksum = "1124AD3AA21143E5AF0FC5CF3D9529F6DB8CA03E43A55711B60B6B7B3874375C" }, { name = "gleam_stdlib", version = "0.54.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "723BA61A2BAE8D67406E59DD88CEA1B3C3F266FC8D70F64BE9FEC81B4505B927" }, { name = "gleeunit", version = "1.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "0E6C83834BA65EDCAAF4FE4FB94AC697D9262D83E6F58A750D63C9F6C8A9D9FF" }, ] [requirements] -gleam_erlang = { version = ">= 1.3.0 and < 2.0.0" } gleam_stdlib = { version = ">= 0.34.0 and < 2.0.0" } gleeunit = { version = ">= 1.0.0 and < 2.0.0" } diff --git a/src/rexen.gleam b/src/rexen.gleam index af05e2c..7c8af3d 100644 --- a/src/rexen.gleam +++ b/src/rexen.gleam @@ -47,8 +47,3 @@ pub fn new(expression: String) -> Result(machine.NFA, String) { pub fn compute(engine: machine.NFA, input: String) -> Bool { machine.evaluate(engine, input) } - -pub fn main() { - let assert Ok(nfa) = new("(a*)*") - echo compute(nfa, "b") -} diff --git a/src/rexen/nfa/machine.gleam b/src/rexen/nfa/machine.gleam index eafff10..b175003 100644 --- a/src/rexen/nfa/machine.gleam +++ b/src/rexen/nfa/machine.gleam @@ -101,9 +101,9 @@ fn evaluate_loop( process_transitions( nfa, node.state.transitions, - rest, + node.index, visited_nodes, - node, + rest, char, ) @@ -116,9 +116,9 @@ fn evaluate_loop( process_transitions( nfa, node.state.transitions, - rest, + node.index, visited_nodes, - node, + rest, char, ) @@ -132,33 +132,39 @@ fn evaluate_loop( fn process_transitions( nfa: NFA, transitions: List(state.Transition), - nodes: List(Node), + index: Int, visited_nodes: Set(Node), - node: Node, + pending_nodes: List(Node), char: String, ) -> List(Node) { case transitions { - [] -> nodes + [] -> pending_nodes [#(matcher, name), ..rest] -> { case state.matches(matcher, char) { False -> { - process_transitions(nfa, rest, nodes, visited_nodes, node, char) + process_transitions( + nfa, + rest, + index, + visited_nodes, + pending_nodes, + char, + ) } True -> { - let index = case state.is_epsilon(matcher) { - True -> node.index - False -> node.index + 1 - } - let assert Ok(to) = dict.get(nfa.states, name) - let new_node = Node(index: index, state: to) + + let new_node = case state.is_epsilon(matcher) { + True -> Node(index: index, state: to) + False -> Node(index: index + 1, state: to) + } let nodes = case set.contains(visited_nodes, new_node) { - True -> nodes - False -> list.append(nodes, [new_node]) + True -> pending_nodes + False -> list.append(pending_nodes, [new_node]) } - process_transitions(nfa, rest, nodes, visited_nodes, node, char) + process_transitions(nfa, rest, index, visited_nodes, nodes, char) } } } From 75e284fd399fe21f4fe107cc874aee70a05cbade Mon Sep 17 00:00:00 2001 From: Tonie Victor Date: Sun, 1 Mar 2026 14:01:50 +0100 Subject: [PATCH 3/3] cleanup --- test/rexen_test.gleam | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 test/rexen_test.gleam diff --git a/test/rexen_test.gleam b/test/rexen_test.gleam new file mode 100644 index 0000000..ecd12ad --- /dev/null +++ b/test/rexen_test.gleam @@ -0,0 +1,5 @@ +import gleeunit + +pub fn main() { + gleeunit.main() +}