From 60eed3180d25fd63b1ee73ddb0ac7125a36af4a8 Mon Sep 17 00:00:00 2001 From: Fadion Dashi Date: Sat, 22 Jul 2017 03:10:39 +0200 Subject: [PATCH] Add file imports Signed-off-by: Fadion Dashi --- README.md | 44 ++++++++++++++++++++++++++- ast/ast.go | 22 ++++++++++++-- interpreter/interpreter.go | 62 ++++++++++++++++++++++++++++++++++++-- lexer/lexer.go | 1 + parser/parser.go | 20 ++++++++++++ token/token.go | 1 + 6 files changed, 144 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index ab86ac1..096eb04 100644 --- a/README.md +++ b/README.md @@ -38,6 +38,7 @@ IO.puts(pipe) // "Expressive Aria Language" * [Pipe Operator](#pipe-operator) * [Immutability](#immutability) * [Modules](#modules) +* [Imports](#imports) * [Comments](#comments) * [Standard Library](#standard-library) @@ -441,6 +442,47 @@ There can't be any other statement in modules except `let`, but those variables Keep in mind that the Aria interpreter is single pass and as such, it will only recognize calls to a module that has already been declared. +## Imports + +Source file imports are a good way of breaking down projects into smaller, easily digestible files. There's no special syntax or rules to imported files. They're included in the caller's scope and treated as if they were originally there. + +```swift +// dog.ari +let name = "Charlie" +let bark_to = fn x + "woof-woof " + x +end +``` + +```swift +// main.ari +import "dog" + +let phrase = name + " " + bark_to("John") +IO.puts(phrase) // "Charlie woof-woof John" +``` + +The file is relatively referenced from the caller and this case, both `main.ari` and `dog.ari` reside in the same folder. As the long as the extension is `.ari`, there's no need to write it in the import statement. + +A more useful pattern would be to wrap imported files into a module. That would make for a more intuitive system and prevent scope leakage. The dog case above would be written simply into: + +```swift +// dog.ari +module Dog + let name = "Charlie" + let bark_to = fn x + "woof-woof " + x + end +end +``` + +```swift +// main.ari +import "dog" + +let phrase = Dog.name + " " + Dog.bark_to("John") +``` + ## Comments Nothing fancy in here! You can comment your code using both inline or block comments: @@ -466,7 +508,7 @@ In the near future, hopefully, I plan to: - Improve the Standard Library with more functions. - Support closures and ~~recursion~~. - Add a short syntax for functions in the form of `x -> x`. -- Add importing of other files. +- ~~Add importing of other files~~. - ~~Add the pipe operator!~~ - Support optional values for null returns. - Write more tests! diff --git a/ast/ast.go b/ast/ast.go index bf14063..5243903 100644 --- a/ast/ast.go +++ b/ast/ast.go @@ -174,8 +174,8 @@ func (e *Subscript) Inspect() string { // Pipe operator. type Pipe struct { Token token.Token - Left Expression - Right Expression + Left Expression + Right Expression } func (e *Pipe) expression() {} @@ -468,6 +468,24 @@ func (e *Continue) TokenLexeme() string { return e.Token.Lexeme } func (e *Continue) TokenLocation() token.Location { return e.Token.Location } func (e *Continue) Inspect() string { return e.Token.Lexeme } +// Import a file. +type Import struct { + Token token.Token + File *String +} + +func (e *Import) expression() {} +func (e *Import) TokenLexeme() string { return e.Token.Lexeme } +func (e *Import) TokenLocation() token.Location { return e.Token.Location } +func (e *Import) Inspect() string { + var out *bytes.Buffer + + out.WriteString("Import ") + out.WriteString(e.File.Value) + + return out.String() +} + // ExpressionStatement as a statement that // holds expressions. type ExpressionStatement struct { diff --git a/interpreter/interpreter.go b/interpreter/interpreter.go index 7a27417..57d5a6c 100644 --- a/interpreter/interpreter.go +++ b/interpreter/interpreter.go @@ -6,6 +6,11 @@ import ( "github.com/fadion/aria/reporter" "math" "strings" + "io/ioutil" + "github.com/fadion/aria/lexer" + "github.com/fadion/aria/reader" + "github.com/fadion/aria/parser" + "path/filepath" ) // Interpreter represents the interpreter. @@ -14,6 +19,7 @@ type Interpreter struct { library *Library functions map[string]string moduleCache map[string]*Scope + importCache map[string]*ast.Program } // New initializes an Interpreter. @@ -26,6 +32,7 @@ func New() *Interpreter { library: lib, functions: map[string]string{}, moduleCache: map[string]*Scope{}, + importCache: map[string]*ast.Program{}, } } @@ -86,6 +93,8 @@ func (i *Interpreter) Interpret(node ast.Node, scope *Scope) DataType { return &BreakType{} case *ast.Continue: return &ContinueType{} + case *ast.Import: + return i.runImport(node, scope) } return nil @@ -443,9 +452,8 @@ func (i *Interpreter) runForDictionary(node *ast.For, dictionary *DictionaryType func (i *Interpreter) runFunction(node *ast.FunctionCall, scope *Scope) DataType { var fn DataType - // There are two possible types of function calls: - // 1. Regular functions: add(1, 2) - // 2. Module access: Store.get(10) + // ModuleAccess is handled differently from + // regular functions calls. switch nodeType := node.Function.(type) { case *ast.ModuleAccess: // Standard library functions use the same dot @@ -468,6 +476,12 @@ func (i *Interpreter) runFunction(node *ast.FunctionCall, scope *Scope) DataType return nil } + // Make sure it's a function we're calling. + if fn.Type() != FUNCTION_TYPE { + i.reportError(node, "Trying to call a non-function") + return nil + } + function := fn.(*FunctionType) arguments := []DataType{} @@ -625,6 +639,39 @@ func (i *Interpreter) runPipe(node *ast.Pipe, scope *Scope) DataType { return nil } +// Import "filename" by reading, lexing and +// parsing it all over. +func (i *Interpreter) runImport(node *ast.Import, scope *Scope) DataType { + filename := i.prepareImportFilename(node.File.Value) + + // Check the cache fist. + if cache, ok := i.importCache[filename]; ok { + return i.Interpret(cache, scope) + } + + source, err := ioutil.ReadFile(i.prepareImportFilename(filename)) + if err != nil { + i.reportError(node, fmt.Sprintf("Couldn't read imported file '%s'", node.File.Value)) + return nil + } + + lex := lexer.New(reader.New(source)) + if reporter.HasErrors() { + return nil + } + + parse := parser.New(lex) + program := parse.Parse() + if reporter.HasErrors() { + return nil + } + + // Cache the parsed program. + i.importCache[filename] = program + + return i.Interpret(program, scope) +} + // Interpret prefix operators: (OP)OBJ func (i *Interpreter) runPrefix(node *ast.PrefixExpression, scope *Scope) DataType { object := i.Interpret(node.Right, scope) @@ -1060,6 +1107,15 @@ func (i *Interpreter) isTruthy(object DataType) bool { } } +func (i *Interpreter) prepareImportFilename(file string) string { + ext := filepath.Ext(file) + if ext == "" { + file = file + ".ari" + } + + return file +} + // Convert a type to an ast.Expression. func (i *Interpreter) typeToExpression(object DataType) ast.Expression { switch value := object.(type) { diff --git a/lexer/lexer.go b/lexer/lexer.go index 3586834..afa7694 100644 --- a/lexer/lexer.go +++ b/lexer/lexer.go @@ -49,6 +49,7 @@ func New(reader *reader.Reader) *Lexer { l.symbol.Insert("break", token.BREAK) l.symbol.Insert("continue", token.CONTINUE) l.symbol.Insert("module", token.MODULE) + l.symbol.Insert("import", token.IMPORT) // Move to the first token. l.advance() diff --git a/parser/parser.go b/parser/parser.go index f32e266..dfaa759 100644 --- a/parser/parser.go +++ b/parser/parser.go @@ -36,6 +36,7 @@ func New(l *lexer.Lexer) *Parser { p.registerPrefix(token.SWITCH, p.parseSwitch) p.registerPrefix(token.FOR, p.parseFor) p.registerPrefix(token.FUNCTION, p.parseFunction) + p.registerPrefix(token.IMPORT, p.parseImport) p.registerPrefix(token.LBRACK, p.parseArrayOrDictionary) p.registerPrefix(token.IDENTIFIER, p.parseIdentifier) p.registerPrefix(token.INTEGER, p.parseInteger) @@ -600,6 +601,25 @@ func (p *Parser) parseFunctionCall(function ast.Expression) ast.Expression { return expression } +// IMPORT STRING +func (p *Parser) parseImport() ast.Expression { + expression := &ast.Import{Token: p.token} + p.advance() + file := p.parseExpression(LOWEST) + + // Import needs a string as the filename + // to be imported. + switch fileString := file.(type) { + case *ast.String: + expression.File = fileString + default: + p.reportError("IMPORT expects a string as filename") + return nil + } + + return expression +} + // Find out if it's an array or a dictionary. func (p *Parser) parseArrayOrDictionary() ast.Expression { p.advance() diff --git a/token/token.go b/token/token.go index 49bf23c..560fcb5 100644 --- a/token/token.go +++ b/token/token.go @@ -79,6 +79,7 @@ const ( BREAK = "BREAK" CONTINUE = "CONTINUE" MODULE = "MODULE" + IMPORT = "IMPORT" // Misc COMMENT = "COMMENT"