Files
kerolox/tree-sitter-kerolox/grammar.js
2026-04-15 08:37:09 -06:00

158 lines
4.4 KiB
JavaScript

/**
* @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 <https://www.gnu.org/licenses/>.
*/
/// <reference types="tree-sitter-cli/dsl" />
// @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("&&", "||")),
),
}
});