Skip to content
Draft

ANTLR #309

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@
.sbt
smithyql-log.txt
.version
**/.antlr
6 changes: 6 additions & 0 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,12 @@ lazy val parser = module("parser")
"co.fs2" %% "fs2-io" % "3.10.2" % Test,
)
)
.enablePlugins(Antlr4Plugin)
.settings(
Antlr4 / antlr4Version := "4.13.0",
Antlr4 / antlr4PackageName := Some("playground.smithyql.parser.v3"),
Antlr4 / antlr4GenVisitor := true,
)
.dependsOn(
ast % "test->test;compile->compile",
source % "test->test;compile->compile",
Expand Down
2 changes: 1 addition & 1 deletion flake.nix
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
overlays = [
(final: prev:
let
jre = final.openjdk11;
jre = final.openjdk17;
jdk = jre;
in
{ inherit jdk jre; })
Expand Down
38 changes: 38 additions & 0 deletions modules/parser/src/main/antlr4/SmithyQL.g4
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
parser grammar SmithyQL;
options {
tokenVocab = Tokens;
}

// todos: comments, forced newlines (e.g. separate use clauses)

qualified_identifier: ident ('.' ident)* '#' ident;

soft_keyword: 'use' | 'service';

ident: ID | soft_keyword;

use_clause: 'use' 'service' qualified_identifier;

prelude: use_clause*;

service_reference: (qualified_identifier '.')? ident;

query_operation_name: service_reference;

number: NUMBER;
bool: 'true' | 'false';
node: number | bool | STRING | NULL | struct | listed;

field: key = ident (':' | '=') value = node;

struct: '{' (field (',' field)* (',')?)? '}';

listed: '[' (node (',' node)* (',')?)? ']';

query: query_operation_name struct;

run_query: query;

statement: run_query;

source_file: prelude statement* EOF;
42 changes: 42 additions & 0 deletions modules/parser/src/main/antlr4/Tokens.g4
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
lexer grammar Tokens;

DOT: '.';
COMMA: ',';
USE: 'use';
SERVICE: 'service';
HASH: '#';
LBRACE: '{';
RBRACE: '}';
LBRACKET: '[';
RBRACKET: ']';
EQUAL: '=';
COLON: ':';
TRUE: 'true';
FALSE: 'false';
NULL: 'null';

ID: [a-zA-Z][a-zA-Z_0-9]*;

// string and number tokens stolen from JSON
// https://github.com/antlr/grammars-v4/blob/b2a35350cbce75b2d47c659ccbadba78a89310ef/json/JSON.g4
STRING: '"' (ESC | SAFECODEPOINT)* '"';

fragment ESC: '\\' (["\\/bfnrt] | UNICODE);

fragment UNICODE: 'u' HEX HEX HEX HEX;

fragment HEX: [0-9a-fA-F];

fragment SAFECODEPOINT: ~ ["\\\u0000-\u001F];

NUMBER: '-'? INT ('.' [0-9]+)? EXP?;

fragment INT: // integer part forbids leading 0s (e.g. `01`)
'0'
| [1-9] [0-9]*;

// no leading zeros
fragment EXP: // exponent number permits leading 0s (e.g. `1e01`)
[Ee] [+-]? [0-9]+;

WS: [ \t\n\r]+ -> skip;
10 changes: 10 additions & 0 deletions modules/parser/src/main/antlr4/Yikes.g4
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
parser grammar Yikes;
options {
tokenVocab = Tokens;
}

namespace: (ID ('.' ID)*);
qualified_identifier: namespace '#' ID;
use_clause: 'use' 'service' qualified_identifier;

source_file: use_clause* EOF;
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
import scala.reflect.ClassTag
/* vocab:

DOT: '.';
USE: 'use';
SERVICE: 'service';
HASH: '#';
ID: [a-zA-Z][a-zA-Z_0-9]*;

*/

/* grammar:

namespace: (ID ('.' ID)*);
qualified_identifier: namespace '#' ID;
use_clause: 'use' 'service' qualified_identifier;

source_file: use_clause* EOF;

*/

sealed trait Token extends Product with Serializable

abstract class SimpleToken(
name: String
) extends Token
with PartialFunction[Token, Unit] {

override def toString(
): String = name

override def isDefinedAt(
x: Token
): Boolean = x == this

override def apply(
v1: Token
): Unit = ()

}

case object Dot extends SimpleToken("Dot")
case object Use extends SimpleToken("Use")
case object Service extends SimpleToken("Service")
case object Hash extends SimpleToken("Hash")

case class Id(
value: String
) extends Token

def tokenize(
input: String
): List[Token] = {
// split by whitespace or punctuation, make sure punctuation is its own token
val tokens = input.split("\\s+|(?=[#\\.])|(?<=[#\\.])(?!\\s)").toList
tokens.flatMap {
case "use" => List(Use)
case "service" => List(Service)
case "#" => List(Hash)
case "." => List(Dot)
case id => List(Id(id))
}
}

var tokens = List.empty[Token]

def previewToken(
) = tokens.headOption

def expectTyped[T <: Token: ClassTag](
): T = expect(TypedMatcher[T](scala.reflect.classTag[T]))

case class TypedMatcher[T](
ct: ClassTag[T]
) extends PartialFunction[Token, T] {

override def apply(
v1: Token
): T = ct.unapply(v1).get

override def isDefinedAt(
x: Token
): Boolean = ct.runtimeClass.isInstance(x)

}

def expect[T](
t: PartialFunction[Token, T]
): T =
previewToken().flatMap { tok =>
nextToken(): Unit
t.lift(tok)
} match {
case Some(p) => p
case p => throw new Exception(s"expected $t, got $p")
}

def nextToken(
) = {
val t = tokens.head
tokens = tokens.tail
t
}

case class QualifiedIdentifier(
namespace: List[Id],
service: Id,
)

case class UseClause(
ident: QualifiedIdentifier
)

case class SourceFile(
clauses: List[UseClause]
)

def qualifiedIdentifier: QualifiedIdentifier = {
val initId = expectTyped[Id]()
var namespace = List(initId)

while (previewToken().contains(Dot)) {
expect(Dot): Unit
namespace = namespace :+ expectTyped[Id]()
}

expect(Hash): Unit

val service = expectTyped[Id]()

QualifiedIdentifier(namespace, service)
}

def useClause: UseClause = {
expect(Use): Unit
expect(Service): Unit
UseClause(qualifiedIdentifier)
}

def sourceFile: SourceFile = {
var clauses = List.empty[UseClause]

while (previewToken().contains(Use))
clauses = clauses :+ useClause

SourceFile(clauses)
}

val example =
"""use service foo.bar#Baz
|use service foo.bar#Quux""".stripMargin

tokens = tokenize(example)
tokens
sourceFile
Loading