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
27 changes: 27 additions & 0 deletions src/internal/lexer/lexer.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,13 @@ var (
type Metadata struct {
Namespace string
Tags []string
Entries []MetadataEntry
}

type MetadataEntry struct {
Key string
Value string
Span diagnostic.Span
}

type Result struct {
Expand Down Expand Up @@ -457,6 +464,7 @@ func (lexer *lexer) scanMetadata(comment string) {
}

lexer.metadata.Namespace = value
lexer.addMetadataEntry(key, value, len(comment))
return
}

Expand All @@ -468,6 +476,25 @@ func (lexer *lexer) scanMetadata(comment string) {
}
}
lexer.metadata.Tags = tags
lexer.addMetadataEntry(key, strings.Join(tags, ", "), len(comment))
}

func (lexer *lexer) addMetadataEntry(key string, value string, length int) {
startLine, startColumn, _ := lexer.file.Map.LineColumn(lexer.start)
endOffset := lexer.start + length
endLine, endColumn, _ := lexer.file.Map.LineColumn(endOffset)
lexer.metadata.Entries = append(lexer.metadata.Entries, MetadataEntry{
Key: key,
Value: value,
Span: diagnostic.Span{
StartLine: startLine,
StartColumn: startColumn,
EndLine: endLine,
EndColumn: endColumn,
StartOffset: lexer.start,
EndOffset: endOffset,
},
})
}

func (lexer *lexer) emitNewline() {
Expand Down
11 changes: 11 additions & 0 deletions src/internal/lexer/lexer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,17 @@ func TestLexCollectsFrontMatterMetadata(t *testing.T) {
if result.Metadata.Tags[0] != "load" || result.Metadata.Tags[1] != "tick" {
t.Fatalf("expected tags [load tick], got %v", result.Metadata.Tags)
}
if len(result.Metadata.Entries) != 2 {
t.Fatalf("expected two metadata entries, got %d", len(result.Metadata.Entries))
}
namespace := result.Metadata.Entries[0]
if namespace.Key != "namespace" || namespace.Value != "example" || namespace.Span.StartOffset != 1 || namespace.Span.EndOffset != 21 {
t.Fatalf("unexpected namespace entry: %#v", namespace)
}
tags := result.Metadata.Entries[1]
if tags.Key != "tags" || tags.Value != "load, tick" || tags.Span.StartLine != 3 || tags.Span.EndLine != 3 {
t.Fatalf("unexpected tags entry: %#v", tags)
}

assertTokenTypes(t, result.Tokens, []token.Type{
token.On,
Expand Down
244 changes: 244 additions & 0 deletions src/internal/parser/parser.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,244 @@
package parser

import (
"fmt"

"github.com/puff-lang/puff/internal/ast"
"github.com/puff-lang/puff/internal/diagnostic"
"github.com/puff-lang/puff/internal/lexer"
"github.com/puff-lang/puff/internal/source"
"github.com/puff-lang/puff/internal/token"
)

type Result struct {
File *ast.File
Diagnostics []diagnostic.Diagnostic
}

type parser struct {
file source.File
tokens []token.Token
current int
diagnostics []diagnostic.Diagnostic
}

func Parse(file source.File, lexed lexer.Result) Result {
parser := &parser{
file: file,
tokens: lexed.Tokens,
diagnostics: append([]diagnostic.Diagnostic(nil), lexed.Diagnostics...),
}

return Result{
File: parser.parseFile(lexed.Metadata),
Diagnostics: parser.diagnostics,
}
}

func (parser *parser) parseFile(metadata lexer.Metadata) *ast.File {
file := &ast.File{
NodeBase: ast.NodeBase{SourceSpan: parser.spanFromOffsets(0, len(parser.file.Text))},
Metadata: metadataEntries(metadata),
}

parser.skipNewlines()
seenDeclaration := false
for !parser.atEnd() {
switch {
case parser.check(token.Require) && !seenDeclaration:
file.Requirements = append(file.Requirements, parser.parseRequire())
case parser.check(token.Require):
parser.reportUnexpected(parser.peek(), "")
parser.synchronizeLine()
case parser.check(token.Fun):
seenDeclaration = true
file.Declarations = append(file.Declarations, parser.parseFunction(false))
case parser.check(token.On):
seenDeclaration = true
file.Declarations = append(file.Declarations, parser.parseEvent())
case parser.check(token.Dollar):
seenDeclaration = true
file.Declarations = append(file.Declarations, parser.parseGlobal(false))
case parser.check(token.Pub):
seenDeclaration = true
if parser.peekNext().Type == token.Fun {
file.Declarations = append(file.Declarations, parser.parseFunction(true))
} else if parser.peekNext().Type == token.Dollar {
file.Declarations = append(file.Declarations, parser.parseGlobal(true))
} else {
parser.reportUnexpected(parser.peek(), "")
parser.synchronizeLine()
}
case parser.check(token.Else), parser.check(token.End):
hint := ""
if parser.check(token.Else) {
hint = "else can only appear inside an if block."
}
parser.reportUnexpected(parser.peek(), hint)
parser.synchronizeLine()
default:
block := parser.check(token.If) || parser.check(token.Loop)
parser.reportInvalidTopLevel()
if block {
parser.synchronizeInvalidBlock()
}
}
parser.skipNewlines()
}

return file
}

func metadataEntries(metadata lexer.Metadata) []ast.MetadataEntry {
entries := make([]ast.MetadataEntry, 0, len(metadata.Entries))
for _, entry := range metadata.Entries {
entries = append(entries, ast.MetadataEntry{
NodeBase: ast.NodeBase{SourceSpan: entry.Span},
Key: entry.Key,
Value: entry.Value,
})
}
return entries
}

func (parser *parser) reportExpected(expected string, hint string) {
tok := parser.peek()
parser.report(
diagnostic.CodeExpectedToken,
fmt.Sprintf("Expected %q.", expected),
hint,
tok.StartOffset,
tok.EndOffset,
)
}

func (parser *parser) reportUnexpected(tok token.Token, hint string) {
parser.report(
diagnostic.CodeUnexpectedToken,
fmt.Sprintf("Unexpected token: %s", tok.Lexeme),
hint,
tok.StartOffset,
tok.EndOffset,
)
}

func (parser *parser) reportInvalidTopLevel() {
start := parser.peek().StartOffset
end := start
for !parser.check(token.Newline) && !parser.atEnd() {
end = parser.advance().EndOffset
}
parser.report(
diagnostic.CodeInvalidTopLevelStatement,
"Executable statements are not allowed at the top level.",
"Move this statement into an event or function.",
start,
end,
)
}

func (parser *parser) report(code diagnostic.Code, message string, hint string, start int, end int) {
parser.diagnostics = append(parser.diagnostics, diagnostic.Diagnostic{
Code: code,
Phase: diagnostic.PhaseParser,
Severity: diagnostic.SeverityError,
Message: message,
Hint: hint,
File: parser.file.RelPath,
Span: parser.spanFromOffsets(start, end),
})
}

func (parser *parser) spanFromOffsets(start int, end int) diagnostic.Span {
startLine, startColumn, _ := parser.file.Map.LineColumn(start)
endLine, endColumn, _ := parser.file.Map.LineColumn(end)
return diagnostic.Span{
StartLine: startLine,
StartColumn: startColumn,
EndLine: endLine,
EndColumn: endColumn,
StartOffset: start,
EndOffset: end,
}
}

func (parser *parser) base(start int, end int) ast.NodeBase {
return ast.NodeBase{SourceSpan: parser.spanFromOffsets(start, end)}
}

func (parser *parser) skipNewlines() {
for parser.match(token.Newline) {
}
}

func (parser *parser) synchronizeLine() {
for !parser.check(token.Newline) && !parser.atEnd() {
parser.advance()
}
}

func (parser *parser) synchronizeInvalidBlock() {
parser.match(token.Newline)
depth := 1
lineStart := true
for !parser.atEnd() && depth > 0 {
if lineStart {
switch parser.peek().Type {
case token.If, token.Loop:
depth++
case token.End:
depth--
}
lineStart = false
}
if parser.match(token.Newline) {
lineStart = true
continue
}
parser.advance()
}
parser.synchronizeLine()
}

func (parser *parser) match(types ...token.Type) bool {
for _, tokenType := range types {
if parser.check(tokenType) {
parser.advance()
return true
}
}
return false
}

func (parser *parser) check(tokenType token.Type) bool {
return parser.peek().Type == tokenType
}

func (parser *parser) advance() token.Token {
if !parser.atEnd() {
parser.current++
}
return parser.tokens[parser.current-1]
}

func (parser *parser) peek() token.Token {
if len(parser.tokens) == 0 {
return token.Token{Type: token.EOF}
}
return parser.tokens[parser.current]
}

func (parser *parser) peekNext() token.Token {
if parser.current+1 >= len(parser.tokens) {
return parser.peek()
}
return parser.tokens[parser.current+1]
}

func (parser *parser) previous() token.Token {
return parser.tokens[parser.current-1]
}

func (parser *parser) atEnd() bool {
return parser.peek().Type == token.EOF
}
12 changes: 12 additions & 0 deletions src/internal/parser/testdata/top_level.golden
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
metadata namespace="example"
metadata tags="load, tick"
require "abc/shop"
require "github.com/123/123" as lib123
global $coins = int(100)
global $shop.name = string("Main Shop")
global $players[] = list()
pub global $tax = float(0.1)
fun setup()
pub fun finalPrice(price: float) -> float
event load
event scoreboard update
22 changes: 22 additions & 0 deletions src/internal/parser/testdata/top_level.puff
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# namespace: example
# tags: load, tick

require "abc/shop"
require "github.com/123/123" as lib123

$coins = 100
$shop.name = "Main Shop"
$players[] = []
pub $tax = 0.1

fun setup
end

pub fun finalPrice(price: float) -> float
end

on load
end

on scoreboard update
end
Loading