I'm working on my own custom language called Elegance. This is my first grammar, and I'm looking for help with:
- Readability
- Expressiveness/DRY code (Can I easily reuse rules/tokens?)
- Edge cases (Is there code that I can't parse that I didn't consider?)
My grammar is far from finished, but I'd appreciate some guidance in its current state so new rules are written well. I've included both the grammar and some sample code I've been using to write my grammar.
Grammar:
grammar Elegance;
@header {
package com.nmerrill.elegance.antlr;
}
file: (package)? (file_import)* (ele_class | function | statement)* EOF;
package :
PACKAGE_DECLARATION IDENTIFIER COLON package_path;
package_path :
IDENTIFIER (PERIOD IDENTIFIER)*;
file_import :
IMPORT_DECLARATION (IDENTIFIER COLON)? package_path;
ele_class :
CLASS_DECLARATION IDENTIFIER
type_parameters?
parameter_list?
type_alias*
OPEN_CURLY
(ele_class | function | assignment)*
CLOSE_CURLY;
function:
FUNCTION_DECLARATION IDENTIFIER parameter_list
COLON type
(MUTABLE_DECLARATION OPEN_PAREN (variable (COMMA variable)*)? CLOSE_PAREN)?
(
OPEN_CURLY (function | statement)* CLOSE_CURLY |
EQUAL function_call
);
variable:
IDENTIFIER (PERIOD IDENTIFIER)*;
assignment:
(VALUE_DECLARATION | VARIABLE_DECLARATION | OPTION_DECLARATION)?
(MUTABLE_DECLARATION)?
IDENTIFIER
(COLON type)?
EQUAL
(expression)
SEMICOLON;
statement:
(function_call | assignment | statement_if | for);
statement_if: IF expression DO statement* (ELSE statement*)? END;
expression_if: IF expression DO expression ELSE expression END;
for: FOR IDENTIFIER IN expression DO statement* END;
function_call: expression argument_list SEMICOLON;
argument_list:
OPEN_PAREN
(argument
(COMMA argument)*
(COMMA named_argument)*
| named_argument
(COMMA named_argument)*
)?
CLOSE_PAREN;
argument: expression;
named_argument: IDENTIFIER EQUAL expression;
parameter_list:
OPEN_PAREN
(parameter
(COMMA parameter)*
)?
CLOSE_PAREN;
parameter:
expression
(COLON type)?;
type:
type ASTERISK | //Iterable type
type COMMA type | //Pair type
type QUESTION_MARK | //Optional type
OPEN_PAREN type_function_parameters CLOSE_PAREN ARROW type | //Named function type
type ARROW type | //Function type
IDENTIFIER
type_parameters?
(OPEN_BRACKET trait_expression CLOSE_BRACKET)?;
type_function_parameters:
IDENTIFIER COLON type (COMMA IDENTIFIER COLON type)* |
type (COMMA type)*;
type_parameters:
(OPEN_ANGLE_BRACKET type (SEMICOLON type)* CLOSE_ANGLE_BRACKET);
trait_expression:
trait_expression OR trait_expression |
trait_expression AND trait_expression |
IDENTIFIER (OPEN_PAREN (literal (COMMA literal)*)? CLOSE_PAREN)?;
type_alias:
WHERE_DECLARATION IDENTIFIER COLON type;
expression:
expression PERIOD IDENTIFIER | //Accessor
expression argument_list | //Function call
expression OR expression |
expression AND expression |
expression (IS | EQ | NEQ) expression |
expression (LEQ | GEQ | LT | GT) expression |
expression (PLUS | MINUS) expression |
expression (ASTERISK | DIV) expression |
expression_if |
OPEN_PAREN expression CLOSE_PAREN |
literal |
type |
IDENTIFIER;
literal: integer | ele_float | list | tuple | dictionary | set | STRING ;
list: OPEN_BRACKET (expression (COMMA expression)* )? CLOSE_BRACKET ;
tuple: TUPLE_START (expression (COMMA expression)* )? CLOSE_BRACKET ;
set: SET_START (expression (COMMA expression)* )? CLOSE_BRACKET ;
dictionary: DICT_START (expression COLON expression (COMMA expression COLON expression)* )? CLOSE_BRACKET ;
integer: (PLUS | MINUS)? INTEGER | ZERO;
ele_float: (PLUS | MINUS)? FLOAT;
INTEGER: [1-9][0-9]* ;
FLOAT: ('0' | [1-9][0-9]* ) '.' [0-9]+;
ZERO: '0' ;
STRING: '"' ('\\'. | ~('"' | '\\'))* '"' | '\'' ('\\'. | ~('\'' | '\\'))* '\'';
SET_START: 's[';
TUPLE_START: 't[';
DICT_START: 'd[';
PACKAGE_DECLARATION: 'package' ;
CLASS_DECLARATION : 'class' ;
FUNCTION_DECLARATION: 'fun' ;
VALUE_DECLARATION: 'val' ;
VARIABLE_DECLARATION: 'var' ;
OPTION_DECLARATION: 'option' ;
IMPORT_DECLARATION: 'import' ;
WHERE_DECLARATION: 'where' ;
MUTABLE_DECLARATION: 'mut' ;
OPEN_CURLY: '{' ;
CLOSE_CURLY: '}' ;
OPEN_PAREN: '(' ;
CLOSE_PAREN: ')';
OPEN_ANGLE_BRACKET: '<' ;
CLOSE_ANGLE_BRACKET: '>';
OPEN_BRACKET: '[' ;
CLOSE_BRACKET: ']' ;
IF: 'if' ;
ELSE: 'else';
FOR: 'for' ;
IN: 'in' ;
DO: 'do' ;
END: 'end';
COLON: ':' ;
COMMA: ',';
PERIOD: '.' ;
EQUAL: '=';
OR: 'or';
AND: 'and';
IS: 'is';
EQ: 'eq';
NEQ: 'neq';
LEQ: 'leq';
GEQ: 'geq';
LT: 'lt';
GT: 'gt';
ASTERISK: '*';
PLUS: '+';
MINUS: '-';
DIV: '/';
SEMICOLON: ';';
IDENTIFIER: [a-zA-Z_][a-zA-Z0-9_]*;
UNDERSCORE: '_';
ARROW : '->';
QUESTION_MARK: '?';
COMMENT: ('//' ~('\r' | '\n')+) -> skip;
WS: ('\n' | '\r' | '\t' | ' ')+ -> skip;
Test Code:
package kothcomm:game //Modules are named: "kothcomm" is the name for the module "com.nmerrill.kothcomm"
import communication.Downloader // No module is necessary for local packages, equivalent to kothcomm:communication.Downloader
import communication.LanguageLoader
import communication.languages.Language
import communication.languages.local.LocalJavaLoader
import game.games.AbstractGame
import game.players.AbstractPlayer
import game.players.Submission
import game.scoring.Aggregator
import game.scoring.ItemAggregator
import game.scoring.Scoreboard
import game.tournaments.Sampling
import game.tournaments.Tournament
import ui.text.TextUI
import core:Random // core is the built-in module
import core:IO
class KotHComm<Player, Game>(gameSupplier: (Void)->Game, gameSize: Int[greaterThan(1)]) //No use of <T,U>, type parameters should be readable
where Player: AbstractPlayer* //Similar to Player extends AbstractPlayer
where Game: AbstractGame<Player> //Doesn't necessarily have to be the first thing in the class
where PlayerSubmission: Submission<Player> //This isn't a type parameter, but "renames" Submission<Player> to PlayerSubmission
{
val mut localLoader = LocalJavaLoader<Player>.new(); //val means the reference doesn't change, mut means that the object can be mutated, private is implied
option languages: Language<Game>* = []; //This one is not required because it has a default value
option scorer: (items: Scoreboard<PlayerSubmission>*)->Scoreboard<PlayerSubmission> = ItemAggregator.new();
//Types! The above is a function that takes a named parameter `items` which is an iterable of Scoreboard<PlayerSubmission> and returns a Scoreboard<PlayerSubmission>
//ItemAggregator.new() is a constructor call, which means that ItemAggregator implements the above function
option tournamentSupplier: (submissions: PlayerSubmission*, random: Random)->Tournament<PlayerSubmission> = Sampling.new;
//Function references can be generated using Sampling.new
option printer: Printer = TextUI.new();
option mut random: Random = Random.new();
option numIterations: Int = 100;
option questionId: Int? = null;
// ? indicates an optional/nullable type
fun addSubmission(name: String, playerSupplier: Void->Player): Void
mut(self)
= localLoader.register(name, playerSupplier);
//This function takes two named parameters, and passes them onto localLoader.register
//LocalLoader.register *does* require named parameters, but because the paramter names match the argument names, no naming is required (you could write name=name if you wanted to)
//mut(self) indicates that the function is impure because it mutates the object
// You should also shorten the above by doing `fun addSubmission = localLoader.register`
fun run(): Void
mut(IO.out,IO.network) //This function mutates the Standard out, and access the network
{
val loader = LanguageLoader<Player>.new(
languages=[localLoader]+languages //List addition! `languages=` is required because its a named parameter
);
if questionId do
Downloader.downloadQuestions(loader, questionId); //Downloader.downloadQuestions mutates IO.network
end
val players = loader.load();
val tournament = tournamentSupplier(submissions = players, random); //To call a function, you simply add the parenthesis
val mut runner = TournamentRunner.new(tournament, aggregator, gameSize, gameSupplier, random); //We'll mutate the runner later, so we need to declare it
printer.printStart(); //This (and other printer calls) mutates IO.out
for i in range(numIterations) do //Range is a function that *doesn't* require named parameters.
//The signature looks like `fun range(ordered stop: Int)`
printer.printProgress(progress=i, total=totalIterations);
runner.createGame().run(); //createGame mutates the runner
end
printer.printFinished();
printer.printScoreboard(runner.scoreboard()); //This also requires a name parameter, scoreboard, but because the method call is `scoreboard`, it isn't needed
}
}
KotHComm.new(gameSupplier = MyGame.new, gameSize = 1) //Creates a KotHComm.