diff --git a/SLY.md b/SLY.md index 1a9ff7e..319f123 100644 --- a/SLY.md +++ b/SLY.md @@ -11,9 +11,14 @@ Comments start with `#` and ignore the rest of the line You can define variables with Sly, the syntax is pretty simple: ``` -variableName = 1; +let variableName = 1; +mut otherVarName = "f"; ``` +A variable declaration is composed of three parts: mutability binding, identifier, and an initial value. + +A variable can either be bound as immutable (`let`) or mutable (`mut`). Immutable variables cannot be reassigned after initialization whereas mutables ones can. + The identifier for the variable must start with a letter but can contain any alphanumeric character subsequently. The possible value types for a variable will be outlined in the Data Types section. @@ -23,11 +28,13 @@ All variable declarations must end with a semicolon. You can use a variable in an attribute declaration or possibly in another variable declaration. ``` -variable1 = "hello"; -variable2 = variable1; +let variable1 = "hello"; +let variable2 = variable1; ``` -Variables are global and are not scoped to slides or blocks. They will be overwritten on reassignment! +Variables are global and are not scoped to slides or blocks. + +Variables may be shadowed by redeclaring them. ## Attribute Declaration diff --git a/examples/basic.sly b/examples/basic.sly index aadee49..7196e64 100644 --- a/examples/basic.sly +++ b/examples/basic.sly @@ -9,9 +9,9 @@ # either HTML, PDF, or (eventually) PPT # Setup our color palette -coolGray = (26, 83, 92); -paleGreen = (247, 255, 247); -tealBlue = (78, 205, 196); +let coolGray = (26, 83, 92); +mut paleGreen = (247, 255, 247); +let tealBlue = (78, 205, 196); # Set the default styles for the rest # of the presentation diff --git a/pkg/lang/compile.go b/pkg/lang/compile.go index d55ea51..87ad3e0 100644 --- a/pkg/lang/compile.go +++ b/pkg/lang/compile.go @@ -8,6 +8,14 @@ import ( "github.com/mbStavola/slydes/pkg/types" ) +var reservedNames = map[string]bool{ + "backgroundColor": true, + "justify": true, + "font": true, + "fontColor": true, + "fontSize": true, +} + type Compiler interface { Compile(statements []Statement) (types.Show, error) } @@ -41,13 +49,13 @@ type compilationState struct { show types.Show slide types.Slide block types.Block - variables map[string]interface{} + variables map[string]variableValue macros map[string][]Statement } func newCompilationState() compilationState { show := types.NewShow() - variables := make(map[string]interface{}) + variables := make(map[string]variableValue) macros := make(map[string][]Statement) var slide = types.NewSlide() @@ -86,17 +94,54 @@ func (cs *compilationState) processStatement(statement Statement) error { case WordBlock: cs.block.Words = statement.data.(string) + break + case VariableDeclaration: + variable := statement.data.(VariableDeclStatement) + if reservedNames[variable.name] { + return tokenErrorInfo(statement.token, compilation, "cannot declare using a reserved name") + } + + value := variableValue{isMutable: variable.isMutable} + + switch data := variable.value.(type) { + case uint8, string, ColorLiteral: + value.value = data + + break + case VariableReference: + dereferenced, err := cs.dereferenceVariable(statement.token, data.reference) + if err != nil { + return err + } + + value.value = dereferenced + } + + cs.variables[variable.name] = value + break case VariableAssignment: variable := statement.data.(VariableStatement) - switch value := variable.value.(type) { + value, ok := cs.variables[variable.name] + if !ok { + return tokenErrorInfo(statement.token, compilation, "cannot assign to undeclared variable") + } else if !value.isMutable { + return tokenErrorInfo(statement.token, compilation, "cannot assign to an immutable binding") + } + + switch data := variable.value.(type) { case uint8, string, ColorLiteral: - cs.variables[variable.name] = value + value.value = data break case VariableReference: - cs.variables[variable.name] = cs.variables[value.reference] + dereferenced, err := cs.dereferenceVariable(statement.token, data.reference) + if err != nil { + return err + } + + value.value = dereferenced } break @@ -107,7 +152,12 @@ func (cs *compilationState) processStatement(statement Statement) error { case "backgroundColor": switch value := attribute.value.(type) { case VariableReference: - c, err := colorFromLiteral(statement.token, cs.variables[value.reference]) + val, err := cs.dereferenceVariable(statement.token, value.reference) + if err != nil { + return err + } + + c, err := colorFromLiteral(statement.token, val) if err != nil { return err } @@ -149,12 +199,16 @@ func (cs *compilationState) processStatement(statement Statement) error { case "font": switch value := attribute.value.(type) { case VariableReference: - val := cs.variables[value.reference] + val, err := cs.dereferenceVariable(statement.token, value.reference) + if err != nil { + return err + } + switch val := val.(type) { case string: cs.block.Style.Font = val default: - return tokenErrorInfo(statement.token, "Font attribute must be a string") + return tokenErrorInfo(statement.token, compilation, "Font attribute must be a string") } break @@ -166,7 +220,12 @@ func (cs *compilationState) processStatement(statement Statement) error { case "fontColor": switch value := attribute.value.(type) { case VariableReference: - c, err := colorFromLiteral(statement.token, cs.variables[value.reference]) + val, err := cs.dereferenceVariable(statement.token, value.reference) + if err != nil { + return err + } + + c, err := colorFromLiteral(statement.token, val) if err != nil { return err } @@ -187,9 +246,14 @@ func (cs *compilationState) processStatement(statement Statement) error { case "fontSize": switch value := attribute.value.(type) { case VariableReference: - size, ok := cs.variables[value.reference].(uint8) + val, err := cs.dereferenceVariable(statement.token, value.reference) + if err != nil { + return err + } + + size, ok := val.(uint8) if !ok { - return tokenErrorInfo(statement.token, "Font size attribute must be an integer") + return tokenErrorInfo(statement.token, compilation, "Font size attribute must be an integer") } cs.block.Style.Size = size @@ -200,10 +264,10 @@ func (cs *compilationState) processStatement(statement Statement) error { break default: - return tokenErrorInfo(statement.token, "Font size attribute must be an integer") + return tokenErrorInfo(statement.token, compilation, "Font size attribute must be an integer") } default: - return tokenErrorInfo(statement.token, "Unrecognized attribute") + return tokenErrorInfo(statement.token, compilation, "Unrecognized attribute") } break @@ -216,7 +280,7 @@ func (cs *compilationState) processStatement(statement Statement) error { macroInvocation := statement.data.(MacroInvocation) macro, ok := cs.macros[macroInvocation.reference] if !ok { - return tokenErrorInfo(statement.token, "Macro not defined") + return tokenErrorInfo(statement.token, compilation, "Macro not defined") } for _, statement := range macro { @@ -229,12 +293,30 @@ func (cs *compilationState) processStatement(statement Statement) error { return nil } +func (cs *compilationState) dereferenceVariable(token Token, name string) (interface{}, error) { + if reservedNames[name] { + return nil, tokenErrorInfo(token, compilation, "cannot dereference a reserved name") + } + + value, ok := cs.variables[name] + if !ok { + return nil, tokenErrorInfo(token, compilation, "variable must be initialized before dereference") + } + + return value.value, nil +} + func (cs *compilationState) finalizeCompilation() types.Show { cs.slide.Blocks = append(cs.slide.Blocks, cs.block) cs.show.Slides = append(cs.show.Slides, cs.slide) return cs.show } +type variableValue struct { + isMutable bool + value interface{} +} + func justificationFromLiteral(token Token, value interface{}) (types.Justification, error) { switch value := value.(type) { case string: @@ -249,7 +331,7 @@ func justificationFromLiteral(token Token, value interface{}) (types.Justificati } message := "Justification attribute must be either 'left', 'right', or 'center'" - return types.Left, tokenErrorInfo(token, message) + return types.Left, tokenErrorInfo(token, compilation, message) } func colorFromLiteral(token Token, value interface{}) (color.Color, error) { @@ -283,7 +365,7 @@ func colorFromLiteral(token Token, value interface{}) (color.Color, error) { }, nil default: message := fmt.Sprintf("Unsupported color '%s'", value) - return nil, tokenErrorInfo(token, message) + return nil, tokenErrorInfo(token, compilation, message) } case ColorLiteral: return color.RGBA{ @@ -293,6 +375,7 @@ func colorFromLiteral(token Token, value interface{}) (color.Color, error) { A: value.a, }, nil default: - return nil, tokenErrorInfo(token, "Color attribute must be either a tuple or string") + fmt.Printf("ff %v\n", value) + return nil, tokenErrorInfo(token, compilation, "Color attribute must be either a tuple or string") } } diff --git a/pkg/lang/errors.go b/pkg/lang/errors.go index 76b3b12..18c8d86 100644 --- a/pkg/lang/errors.go +++ b/pkg/lang/errors.go @@ -34,6 +34,7 @@ func (b ErrorInfoBundle) Error() string { type ErrorInfo struct { line uint location string + stage stage message string } @@ -49,18 +50,31 @@ func lexemeErrorInfo(line uint, lexeme rune, message string) ErrorInfo { return ErrorInfo{ line: line, location: fmt.Sprintf(" at '%c'", lexeme), + stage: lexing, message: message, } } -func tokenErrorInfo(token Token, message string) ErrorInfo { +func tokenErrorInfo(token Token, stage stage, message string) ErrorInfo { return ErrorInfo{ line: token.line, location: fmt.Sprintf(" at '%c'", token.lexeme), + stage: stage, message: message, } } func (err ErrorInfo) Error() string { - return fmt.Sprintf("[line=%d] Error%s: %s", err.line, err.location, err.message) + stage := "" + if err.stage != unspecified { + stage = fmt.Sprintf(" %s ", err.stage) + } + + return fmt.Sprintf( + "[line=%d]%sError%s: %s", + err.line, + stage, + err.location, + err.message, + ) } diff --git a/pkg/lang/lex.go b/pkg/lang/lex.go index 9064329..36e54c0 100644 --- a/pkg/lang/lex.go +++ b/pkg/lang/lex.go @@ -28,6 +28,10 @@ const ( EqualSign Comma + // Keywords + Let + Mut + // Special SlideScope SubScope @@ -56,6 +60,9 @@ func (t TokenType) String() string { "EqualSign", "Comma", + "Let", + "Mut", + "SlideScope", "SubScope", @@ -222,6 +229,38 @@ func processRune(muncher *runeMuncher) (Token, error) { lexeme: char, }, nil + case 'l': + if chars, err := muncher.Peek(2); err == io.EOF { + return Token{}, lexemeErrorInfo(muncher.line, char, "Unexpected end of file") + } else if err != nil { + return Token{}, err + } else if string(chars[:]) == "et" { + muncher.eatN(2) + return Token{ + Type: Let, + line: muncher.line, + lexeme: char, + }, nil + } + + // Intentional fallthrough-- this might be an identifier + + case 'm': + if chars, err := muncher.Peek(2); err == io.EOF { + return Token{}, lexemeErrorInfo(muncher.line, char, "Unexpected end of file") + } else if err != nil { + return Token{}, err + } else if string(chars[:]) == "ut" { + muncher.eatN(2) + return Token{ + Type: Mut, + line: muncher.line, + lexeme: char, + }, nil + } + + // Intentional fallthrough-- this might be an identifier + case '-': if chars, err := muncher.Peek(2); err == io.EOF { return Token{}, lexemeErrorInfo(muncher.line, char, "Unexpected end of file") diff --git a/pkg/lang/parse.go b/pkg/lang/parse.go index 9d16b88..a319514 100644 --- a/pkg/lang/parse.go +++ b/pkg/lang/parse.go @@ -13,6 +13,8 @@ const ( SlideDecl ScopeDecl + VariableDeclaration + VariableAssignment AttributeAssignment MacroAssignment @@ -29,6 +31,8 @@ func (s StatementType) String() string { "SlideDecl", "ScopeDecl", + "VariableDeclaration", + "VariableAssignment", "AttributeAssignment", "MacroAssignment", @@ -44,6 +48,12 @@ type Statement struct { data interface{} } +type VariableDeclStatement struct { + name string + isMutable bool + value interface{} +} + type VariableStatement struct { name string value interface{} @@ -141,10 +151,23 @@ func assignment(muncher *tokenMuncher) (Statement, error) { ty := VariableAssignment token := muncher.peek() - if token.Type == AtSign { + switch token.Type { + case Let: + ty = VariableDeclaration + muncher.eat() + + break + case Mut: + ty = VariableDeclaration + muncher.eat() + + break + case AtSign: ty = AttributeAssignment muncher.eat() - } else if token.Type == DollarSign { + + break + case DollarSign: ty = MacroAssignment muncher.eat() } @@ -202,7 +225,13 @@ func assignment(muncher *tokenMuncher) (Statement, error) { return Statement{}, err } - if ty == VariableAssignment { + if ty == VariableDeclaration { + data = VariableDeclStatement{ + name: identToken.data.(string), + isMutable: token.Type == Mut, + value: value, + } + } else if ty == VariableAssignment { data = VariableStatement{ name: identToken.data.(string), value: value, @@ -227,7 +256,7 @@ func assignment(muncher *tokenMuncher) (Statement, error) { } message := fmt.Sprintf("Unexpected token %s", token.Type.String()) - return Statement{}, tokenErrorInfo(token, message) + return Statement{}, tokenErrorInfo(token, parsing, message) } func colorLiteral(muncher *tokenMuncher) (interface{}, error) { @@ -293,7 +322,7 @@ func value(muncher *tokenMuncher) (interface{}, error) { return VariableReference{reference: token.data.(string)}, nil } - return nil, tokenErrorInfo(token, "Expected value") + return nil, tokenErrorInfo(token, parsing, "Expected value") } func synchronizeFromErrorState(muncher *tokenMuncher) { @@ -334,7 +363,7 @@ func (tm *tokenMuncher) tryEat(expected TokenType) (Token, error) { } message := fmt.Sprintf("Expected %s, but was %s", expected.String(), token.Type.String()) - return Token{}, tokenErrorInfo(token, message) + return Token{}, tokenErrorInfo(token, parsing, message) } func (tm *tokenMuncher) previous() Token { diff --git a/pkg/lang/sly.go b/pkg/lang/sly.go index be557f2..345e6fd 100644 --- a/pkg/lang/sly.go +++ b/pkg/lang/sly.go @@ -7,6 +7,24 @@ import ( "github.com/mbStavola/slydes/pkg/types" ) +type stage int + +const ( + unspecified stage = iota + lexing + parsing + compilation +) + +func (s stage) String() string { + return []string{ + "", + "Lexing", + "Parsing", + "Compilation", + }[s] +} + // This type represents a three phase "compiler" for // the Sly language which can produce an instance of Show type Sly struct { diff --git a/pkg/lang/sly_test.go b/pkg/lang/sly_test.go index 4fa3bc7..f026a93 100644 --- a/pkg/lang/sly_test.go +++ b/pkg/lang/sly_test.go @@ -1,10 +1,11 @@ package lang import ( - "github.com/mbStavola/slydes/pkg/types" "image/color" "strings" "testing" + + "github.com/mbStavola/slydes/pkg/types" ) var sly = NewSly() @@ -13,9 +14,9 @@ func TestSimplePresentation(t *testing.T) { source := ` # This is a very simple slideshow # Hopefully everything works as intended - coolGray = (26, 83, 92); - paleGreen = (247, 255, 247,); - tealBlue = (78, 205, 196, 255); + let coolGray = (26, 83, 92); + let paleGreen = (247, 255, 247,); + let tealBlue = (78, 205, 196, 255); ---Welcome!--- @@ -140,7 +141,7 @@ Text func TestMacro(t *testing.T) { source := ` - red = "red"; + let red = "red"; $styleMacro = { @backgroundColor = red; @fontColor = "blue";