Skip to content

Commit

Permalink
Add file imports
Browse files Browse the repository at this point in the history
Signed-off-by: Fadion Dashi <[email protected]>
  • Loading branch information
fadion committed Jul 22, 2017
1 parent b1eb220 commit 60eed31
Show file tree
Hide file tree
Showing 6 changed files with 144 additions and 6 deletions.
44 changes: 43 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down Expand Up @@ -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:
Expand All @@ -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!
Expand Down
22 changes: 20 additions & 2 deletions ast/ast.go
Original file line number Diff line number Diff line change
Expand Up @@ -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() {}
Expand Down Expand Up @@ -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 {
Expand Down
62 changes: 59 additions & 3 deletions interpreter/interpreter.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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.
Expand All @@ -26,6 +32,7 @@ func New() *Interpreter {
library: lib,
functions: map[string]string{},
moduleCache: map[string]*Scope{},
importCache: map[string]*ast.Program{},
}
}

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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{}

Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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) {
Expand Down
1 change: 1 addition & 0 deletions lexer/lexer.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
20 changes: 20 additions & 0 deletions parser/parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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()
Expand Down
1 change: 1 addition & 0 deletions token/token.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ const (
BREAK = "BREAK"
CONTINUE = "CONTINUE"
MODULE = "MODULE"
IMPORT = "IMPORT"

// Misc
COMMENT = "COMMENT"
Expand Down

0 comments on commit 60eed31

Please sign in to comment.