diff --git a/src/better_parser/SimplePrologParser.kt b/src/better_parser/SimplePrologParser.kt new file mode 100644 index 0000000..26e1a15 --- /dev/null +++ b/src/better_parser/SimplePrologParser.kt @@ -0,0 +1,61 @@ +package better_parser + +import com.github.h0tk3y.betterParse.combinators.* +import com.github.h0tk3y.betterParse.grammar.Grammar +import com.github.h0tk3y.betterParse.grammar.parser +import com.github.h0tk3y.betterParse.lexer.Token +import com.github.h0tk3y.betterParse.lexer.literalToken +import com.github.h0tk3y.betterParse.lexer.regexToken +import com.github.h0tk3y.betterParse.parser.Parser +import prolog.ast.arithmetic.Float +import prolog.ast.arithmetic.Integer +import prolog.ast.terms.Atom +import prolog.ast.terms.Structure +import prolog.ast.terms.Term +import prolog.ast.terms.Variable + +open class SimplePrologParser : Grammar() { + // Prolog tokens + private val nameToken: Token by regexToken("[a-z][a-zA-Z0-9_]*") + private val variableToken: Token by regexToken("[A-Z][a-zA-Z0-9_]*") + + // Arithmetic tokens + private val floatToken: Token by regexToken("-?[1-9][0-9]*\\.[0-9]+") + private val integerToken: Token by regexToken("-?([1-9][0-9]*|0)") + + // Special tokens + private val comma: Token by literalToken(",") + private val leftParenthesis: Token by literalToken("(") + private val rightParenthesis: Token by literalToken(")") + + // Ignored tokens + private val whitespace: Token by regexToken("\\s+", ignore = true) + private val singleLineComment: Token by regexToken("%[^\\n]*", ignore = true) + private val multiLineComment: Token by regexToken("/\\*.*?\\*/", ignore = true) + + // Prolog parsers + private val atomParser: Parser by nameToken use { Atom(text) } + private val variableParser: Parser by variableToken use { Variable(text) } + private val structureParser: Parser by (atomParser and skip(leftParenthesis) and separated( + parser(this::termParser), + comma, + acceptZero = true + ) and skip(rightParenthesis)) use { + Structure(t1, t2.terms) + } + + // Arithmetic parsers + private val integerParser: Parser by integerToken use { Integer(text.toInt()) } + private val floatParser: Parser by floatToken use { + Float(text.toFloat()) + } + + private val termParser: Parser by (floatParser + or integerParser + or variableParser + or structureParser + or atomParser + ) map { it } + + override val rootParser: Parser = termParser +} \ No newline at end of file diff --git a/tests/better_parser/SimplePrologParserTests.kt b/tests/better_parser/SimplePrologParserTests.kt new file mode 100644 index 0000000..4036acd --- /dev/null +++ b/tests/better_parser/SimplePrologParserTests.kt @@ -0,0 +1,156 @@ +package better_parser + +import com.github.h0tk3y.betterParse.grammar.Grammar +import com.github.h0tk3y.betterParse.grammar.parseToEnd +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.ValueSource +import prolog.ast.arithmetic.Float +import prolog.ast.arithmetic.Integer +import prolog.ast.terms.Atom +import prolog.ast.terms.Structure +import prolog.ast.terms.Term +import prolog.ast.terms.Variable +import prolog.logic.equivalent + +class SimplePrologParserTests { + lateinit var parser: Grammar + + @BeforeEach + fun setup() { + parser = SimplePrologParser() as Grammar + } + + @ParameterizedTest + @ValueSource(strings = ["a", "foo", "foo1", "fooBar", "foo_bar"]) + fun `parse atom`(name: String) { + val result = parser.parseToEnd(name) + + assertEquals(Atom(name), result, "Expected atom '$name'") + } + + @ParameterizedTest + @ValueSource(strings = ["X", "X1", "X_1"]) + fun `parse variable`(name: String) { + val result = parser.parseToEnd(name) + + assertEquals(Variable(name), result, "Expected atom '$name'") + } + + @Test + fun `empty compound term`() { + val input = "f()" + + val result = parser.parseToEnd(input) + + assertTrue( + equivalent(Structure(Atom("f"), emptyList()), result, emptyMap()), + "Expected atom 'f'" + ) + } + + @Test + fun `parse compound term f(a)`() { + val input = "f(a)" + + val result = parser.parseToEnd(input) + + assertTrue( + equivalent(Structure(Atom("f"), listOf(Atom("a"))), result, emptyMap()), + "Expected atom 'f(a)'" + ) + } + + @Test + fun `parse compound term f(a, b)`() { + val input = "f(a, b)" + + val result = parser.parseToEnd(input) + + assertTrue( + equivalent(Structure(Atom("f"), listOf(Atom("a"), Atom("b"))), result, emptyMap()), + "Expected atom 'f(a, b)'" + ) + } + + @Test + fun `parse compound term with variable f(a, X)`() { + val input = "f(a, X)" + + val result = parser.parseToEnd(input) + + assertTrue( + equivalent(Structure(Atom("f"), listOf(Atom("a"), Variable("X"))), result, emptyMap()), + "Expected atom 'f(a, X)'" + ) + } + + @Test + fun `parse nested compound term f(a, g(b))`() { + val input = "f(a, g(b))" + + val result = parser.parseToEnd(input) + + assertTrue( + equivalent( + Structure(Atom("f"), listOf(Atom("a"), Structure(Atom("g"), listOf(Atom("b"))))), + result, + emptyMap() + ), + "Expected atom 'f(a, g(b))'" + ) + } + + @Test + fun `parse compound term with variable f(a, g(X))`() { + val input = "f(a, g(X))" + + val result = parser.parseToEnd(input) + + assertTrue( + equivalent( + Structure(Atom("f"), listOf(Atom("a"), Structure(Atom("g"), listOf(Variable("X"))))), + result, + emptyMap() + ), + "Expected atom 'f(a, g(X))'" + ) + } + + @ParameterizedTest + @ValueSource(ints = [-987654321, -543, -21, -1, 0, 1, 5, 12, 345, 123456789]) + fun `parse integer`(number: Int) { + val input = number.toString() + + val result = parser.parseToEnd(input) + + assertEquals(Integer(number), result, "Expected integer '$number'") + } + + @Test + fun `parse float`() { + val input = "42.0" + + val result = parser.parseToEnd(input) + + assertTrue( + equivalent(Float(42.0f), result, emptyMap()), + "Expected float '42.0'" + ) + } + + @Test + fun `parse negative float`() { + val input = "-42.0" + + val result = parser.parseToEnd(input) + + assertTrue( + equivalent(Float(-42.0f), result, emptyMap()), + "Expected float '-42.0'" + ) + } +} \ No newline at end of file