/** * @file Kerolox grammar for tree-sitter * @license AGPL-3.0-or-later * * Copyright (C) 2025-2026 Marceline Cramer * SPDX-License-Identifier: AGPL-3.0-or-later * * Kerolox is free software: you can redistribute it and/or modify it under * the terms of the GNU Affero General Public License as published by the Free * Software Foundation, either version 3 of the License, or (at your option) any * later version. * * Kerolox is distributed in the hope that it will be useful, but WITHOUT ANY * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS * FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for * more details. * * You should have received a copy of the GNU Affero General Public License * along with Kerolox. If not, see . */ /// // @ts-check const list = el => seq(el, repeat(seq(",", el))) const listComma = el => seq(el, choice(",", repeat1(seq(",", el)))) const parenList = (el) => seq("(", list(el), ")"); const parenListComma = (el) => seq("(", listComma(el), ")"); /// Shorthand to write a binary expression with left precedence of the given priority. const expr_prec = (expr, precedence, op) => prec.left(precedence, seq(field("lhs", expr), field("op", op), field("rhs", expr))) export default grammar({ name: "kerolox", extras: $ => [$._whitespace, $.comment], rules: { file: $ => repeat(choice( // explicitly parse newlines and comments separately // this helps determine which comments are doc comments $.newline, $.comment, // the actual items $.type_alias, $.import, $.definition, $.rule, $.assumption )), comment: $ => seq(";", $.commentInner), commentInner: _ => /[^\n]*\n/, newline: _ => /[\n\r]/, _whitespace: _ => /[ \n\r\t]/, docs: _ => /(?:;.*\n)*/, variable: _ => /[a-z][a-zA-Z0-9_]*/, symbol: _ => /[A-Z][a-zA-Z0-9]*/, _ident: $ => choice($.variable, $.symbol), integer: _ => choice("0", /-?[1-9][0-9]*/), type_alias: $ => seq( "type", field("name", $.symbol), "=", field("type", $.type), ), type: $ => choice( field("named", $.symbol), parenListComma(field("tuple", $.type)), ), import: $ => seq( "import", field("path", $.symbol), repeat(seq(".", field("path", $.symbol))), ".", choice( field("item", $.symbol), parenList(field("item", $.symbol)) ), ), definition: $ => seq( "define", field("input", optional("input")), field("output", optional("output")), field("decision", optional("decision")), field("relation", $.symbol), field("type", $.type), ), rule: $ => seq( optional(field("negate", "-")), field("relation", $.symbol), field("head", $.expr), optional(seq(":-", field("body", $.rule_body))), "." ), assumption: $ => seq( optional(seq("soft", "(", field("soft", $.integer), ")")), ":-", field("body", $.rule_body), "." ), rule_body: $ => list(field("clause", $.expr)), expr: $ => choice( field("hole", "_"), field("atom", $.atom), field("tuple", $.tuple), field("value", $.value), field("variable", $.variable), field("aggregate", $.aggregate), field("unary", $.unary_expr), field("binary", $.binary_expr), seq('(', field("parens", $.expr), ')'), ), atom: $ => prec.right(2, seq(field("head", $.symbol), field("body", $.expr))), tuple: $ => parenListComma(field("el", $.expr)), value: $ => choice( field("true", "True"), field("false", "False"), field("symbol", $.symbol), field("integer", $.integer), ), aggregate: $ => seq( field("op", $.aggregate_op), choice( field("atom", $.atom), seq("{", field("body", $.rule_body ), "}"), ), ), aggregate_op: $ => /@[a-z]+/, unary_expr: $ => prec.left(5, seq( field("op", $.unary_op), field("term", $.expr) )), unary_op: _ => choice("!", "-"), binary_expr: $ => choice( expr_prec($.expr, 4, choice("*", "/")), expr_prec($.expr, 3, choice("+", "-")), expr_prec($.expr, 2, ".."), expr_prec($.expr, 1, choice("=", "!=", ">=", "<=", "<", ">")), expr_prec($.expr, 0, choice("&&", "||")), ), } });