Skip to content
Merged
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
141 changes: 141 additions & 0 deletions batteries/syntax/ast_types.luau
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
export type Position = {
line: number,
column: number,
}

export type Location = {
begin: Position,
["end"]: Position, -- TODO: do we really want to have to use brackets everywhere?
}

export type Whitespace = {
tag: "whitespace",
location: Location,
text: string,
}

export type SingleLineComment = {
tag: "comment",
location: Location,
text: string,
}

export type MultiLineComment = {
tag: "blockcomment",
location: Location,
text: string,
-- TODO: depth: number,
}

export type Trivia = Whitespace | SingleLineComment | MultiLineComment

export type Token<Kind = string> = {
read leadingTrivia: { Trivia },
read position: Position,
read text: Kind,
read trailingTrivia: { Trivia },
}

export type Pair<T, Separator> = { node: T, separator: Token<Separator>? }
export type Punctuated<T, Separator = ","> = { Pair<T, Separator> }

export type AstLocal = {
name: Token<string>,
}

export type AstExprConstantString = Token<string> & {
tag: "string",
quoteStyle: "single" | "double" | "block" | "interp",
blockDepth: number,
}

export type AstExprLocal = {
tag: "local",
token: Token<string>,
["local"]: AstLocal,
upvalue: boolean,
}

export type AstExprGlobal = {
tag: "global",
name: Token,
}

export type AstExprCall = {
tag: "call",
func: AstExpr, -- TODO: stricter?
openParens: Token<"(">?,
arguments: Punctuated<AstExpr>,
closeParens: Token<")">?,
}

export type AstExprIndexName = {
tag: "indexname",
expr: AstExpr,
accessor: Token<"." | ":">,
index: Token<string>,
indexLocation: Location,
}

export type AstExprIndexExpr = {
tag: "index",
expr: AstExpr,
index: AstExpr,
}

export type AstExprBinary = {
tag: "binary",
lhsoperand: AstExpr,
operator: Token, -- TODO: enforce token type
rhsoperand: AstExpr,
}

export type AstExprTableItem =
| { kind: "list", value: AstExpr }
| { kind: "record", key: string, equals: Token<"=">, value: AstExpr }
| { kind: "general", key: string, equals: Token<"=">, value: AstExpr }

export type AstExprTable = {
tag: "table",
openBrace: Token<"{">,
entries: { never },
closeBrace: Token<"}">,
}

export type AstExpr =
| AstExprConstantString
| AstExprLocal
| AstExprGlobal
| AstExprCall
| AstExprIndexName
| AstExprIndexExpr
| AstExprBinary
| AstExprTable

export type AstStatBlock = {
tag: "block",
statements: { AstStat },
}

export type AstStatReturn = {
tag: "return",
["return"]: Token<"return">,
expressions: Punctuated<AstExpr>,
}

export type AstStatExpr = {
tag: "expression",
expression: AstExpr,
}

export type AstStatLocal = {
tag: "local",
["local"]: Token<"local">,
variables: Punctuated<AstLocal>,
equals: Token<"=">?,
values: Punctuated<AstExpr>,
}

export type AstStat = AstStatBlock | AstStatReturn | AstStatExpr | AstStatLocal

return {}
13 changes: 13 additions & 0 deletions batteries/syntax/parser.luau
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
--!strict

local luau = require("@lute/luau")
local T = require("./ast_types")

--- Parses Luau source code into an AstStatBlock
local function parse(source: string): T.AstStatBlock
return luau.parse(source).root
end

return {
parse = parse,
}
71 changes: 71 additions & 0 deletions batteries/syntax/printer.luau
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
--!strict
local T = require("./ast_types")
local visitor = require("./visitor")

local function exhaustiveMatch(value: never): never
error(`Unknown value in exhaustive match: {value}`)
end

local function printTrivia(trivia: T.Trivia): string
if trivia.tag == "whitespace" or trivia.tag == "comment" or trivia.tag == "blockcomment" then
return trivia.text
else
return exhaustiveMatch(trivia.tag)
end
end

local function printTriviaList(trivia: { T.Trivia })
local result = ""
for _, trivia in trivia do
result ..= printTrivia(trivia)
end
return result
end

local function printToken(token: T.Token): string
return printTriviaList(token.leadingTrivia) .. token.text .. printTriviaList(token.trailingTrivia)
end

local function printString(expr: T.AstExprConstantString): string
local result = printTriviaList(expr.leadingTrivia)

if expr.quoteStyle == "single" then
result ..= `'{expr.text}'`
elseif expr.quoteStyle == "double" then
result ..= `"{expr.text}"`
elseif expr.quoteStyle == "block" then
local equals = string.rep("=", expr.blockDepth)
result ..= `[{equals}[{expr.text}]{equals}]`
elseif expr.quoteStyle == "interp" then
result ..= "`" .. expr.text .. "`"
else
return exhaustiveMatch(expr.quoteStyle)
end

result ..= printTriviaList(expr.trailingTrivia)
return result
end

--- Returns a string representation of an AstStatBlock
local function printBlock(block: T.AstStatBlock): string
local printer = visitor.createVisitor()
local result = ""

printer.visitToken = function(node: T.Token)
result ..= printToken(node)
return false
end

printer.visitString = function(node: T.AstExprConstantString)
result ..= printString(node)
return false
end

visitor.visitBlock(block, printer)

return result
end

return {
print = printBlock,
}
Loading