Skip to content

Commit

Permalink
feat(ch4): hashes
Browse files Browse the repository at this point in the history
  • Loading branch information
Linkinlog committed Mar 15, 2024
1 parent d51910a commit 3587000
Show file tree
Hide file tree
Showing 14 changed files with 448 additions and 36 deletions.
30 changes: 30 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
name: Release

on:
release:
types: [created]

permissions:
contents: write
packages: write

jobs:
releases-matrix:
name: Release Go Binary
runs-on: ubuntu-latest
strategy:
matrix:
goos: [linux, windows, darwin]
goarch: ["386", amd64, arm64]
exclude:
- goarch: "386"
goos: darwin
- goarch: arm64
goos: windows
steps:
- uses: actions/checkout@v4
- uses: wangyoucao577/go-release-action@v1
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
goos: ${{ matrix.goos }}
goarch: ${{ matrix.goarch }}
22 changes: 22 additions & 0 deletions ast/ast.go
Original file line number Diff line number Diff line change
Expand Up @@ -312,3 +312,25 @@ func (ie *IndexExpression) String() string {

return out.String()
}

type HashLiteral struct {
Token token.Token
Pairs map[Expression]Expression
}

func (hl *HashLiteral) expressionNode() {}
func (hl *HashLiteral) TokenLiteral() string { return hl.Token.Literal }
func (hl *HashLiteral) String() string {
var out bytes.Buffer

pairs := []string{}
for key, value := range hl.Pairs {
pairs = append(pairs, key.String()+":"+value.String())
}

out.WriteString("{")
out.WriteString(strings.Join(pairs, ", "))
out.WriteString("}")

return out.String()
}
22 changes: 15 additions & 7 deletions evaluator/builtins.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ package evaluator
import "github.com/Linkinlog/MagLang/object"

var builtins = map[string]*object.Builtin{
"thickness": &object.Builtin{
"thickness": {
Fn: func(args ...object.Object) object.Object {
if len(args) != 1 {
return newError("wrong number of arguments. got=%d, want=1",
Expand All @@ -21,7 +21,7 @@ var builtins = map[string]*object.Builtin{
}
},
},
"first": &object.Builtin{
"first": {
Fn: func(args ...object.Object) object.Object {
if len(args) != 1 {
return newError("wrong number of arguments. got=%d, want=1",
Expand All @@ -40,7 +40,7 @@ var builtins = map[string]*object.Builtin{
return NULL
},
},
"last": &object.Builtin{
"last": {
Fn: func(args ...object.Object) object.Object {
if len(args) != 1 {
return newError("wrong number of arguments. got=%d, want=1",
Expand All @@ -60,7 +60,7 @@ var builtins = map[string]*object.Builtin{
return NULL
},
},
"bum": &object.Builtin{
"bum": {
Fn: func(args ...object.Object) object.Object {
if len(args) != 1 {
return newError("wrong number of arguments. got=%d, want=1",
Expand All @@ -74,15 +74,15 @@ var builtins = map[string]*object.Builtin{
arr := args[0].(*object.Array)
length := len(arr.Elements)
if length > 0 {
newElements := make([]object.Object, length-1, length-1)
newElements := make([]object.Object, length-1)
copy(newElements, arr.Elements[1:length])
return &object.Array{Elements: newElements}
}

return NULL
},
},
"push": &object.Builtin{
"push": {
Fn: func(args ...object.Object) object.Object {
if len(args) != 2 {
return newError("wrong number of arguments. got=%d, want=2",
Expand All @@ -96,11 +96,19 @@ var builtins = map[string]*object.Builtin{
arr := args[0].(*object.Array)
length := len(arr.Elements)

newElements := make([]object.Object, length+1, length+1)
newElements := make([]object.Object, length+1)
copy(newElements, arr.Elements)
newElements[length] = args[1]

return &object.Array{Elements: newElements}
},
},
"log": {
Fn: func(args ...object.Object) object.Object {
for _, arg := range args {
println(arg.Inspect())
}
return NULL
},
},
}
45 changes: 45 additions & 0 deletions evaluator/evaluator.go
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,8 @@ func Eval(node ast.Node, env *object.Environment) object.Object {
return index
}
return evalIndexExpression(left, index)
case *ast.HashLiteral:
return evalHashLiteral(node, env)
}

return nil
Expand Down Expand Up @@ -336,6 +338,8 @@ func evalIndexExpression(left, index object.Object) object.Object {
switch {
case left.Type() == object.ARRAY_OBJ && index.Type() == object.INTEGER_OBJ:
return evalArrayIndexExpression(left, index)
case left.Type() == object.HASH_OBJ:
return evalHashIndexExpression(left, index)
default:
return newError("index operator not supported: %s", left.Type())
}
Expand All @@ -353,6 +357,47 @@ func evalArrayIndexExpression(array, index object.Object) object.Object {
return arrayObject.Elements[idx]
}

func evalHashLiteral(node *ast.HashLiteral, env *object.Environment) object.Object {
pairs := make(map[object.HashKey]object.HashPair)

for keyNode, valueNode := range node.Pairs {
key := Eval(keyNode, env)
if isError(key) {
return key
}

hashKey, ok := key.(object.Hashable)
if !ok {
return newError("unusable as hash key: %s", key.Type())
}

value := Eval(valueNode, env)
if isError(value) {
return value
}

hashed := hashKey.HashKey()
pairs[hashed] = object.HashPair{Key: key, Value: value}
}

return &object.Hash{Pairs: pairs}
}

func evalHashIndexExpression(hash, index object.Object) object.Object {
hashObject := hash.(*object.Hash)
key, ok := index.(object.Hashable)
if !ok {
return newError("unusable as hash key: %s", index.Type())
}

pair, ok := hashObject.Pairs[key.HashKey()]
if !ok {
return NULL
}

return pair.Value
}

func newError(format string, a ...any) *object.Error {
return &object.Error{Message: fmt.Sprintf(format, a...)}
}
72 changes: 71 additions & 1 deletion evaluator/evaluator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -393,7 +393,7 @@ func TestBuiltinFunctions(t *testing.T) {
{`bum([])`, nil},
{`push([], 1)`, []int{1}},
{`push(1, 1)`, "argument to `push` must be ARRAY, got INTEGER"},
// {`puts("hello", "world!")`, nil},
{`log("hello", "world!")`, nil},
}

for _, tt := range tests {
Expand Down Expand Up @@ -483,3 +483,73 @@ func TestArrayIndexExpressions(t *testing.T) {
})
}
}

func TestHashLiterals(t *testing.T) {
t.Parallel()
input := `ask two = "two"; {
"one": 10 - 9,
two: 1 + 1,
"thr" + "ee": 6 / 2,
4: 4,
5: 5,
fact: 5,
cap: 6
}`

evaluated := testEval(input)
result, ok := evaluated.(*object.Hash)
if !ok {
t.Fatalf("Eval didn't return Hash. got=%T (%+v)", evaluated, evaluated)
}

expected := map[object.HashKey]int64{
(&object.String{Value: "one"}).HashKey(): 1,
(&object.String{Value: "two"}).HashKey(): 2,
(&object.String{Value: "three"}).HashKey(): 3,
(&object.Integer{Value: 4}).HashKey(): 4,
(&object.Integer{Value: 5}).HashKey(): 5,
TRUE.HashKey(): 5,
FALSE.HashKey(): 6,
}

if len(result.Pairs) != len(expected) {
t.Fatalf("hash has wrong num of pairs. got=%d", len(result.Pairs))
}

for expectedKey, expectedValue := range expected {
pair, ok := result.Pairs[expectedKey]
if !ok {
t.Errorf("no pair for given key in Pairs")
}
testIntegerObject(t, pair.Value, expectedValue)
}
}

func TestHashIndexExpressions(t *testing.T) {
t.Parallel()
tests := []struct {
input string
expected interface{}
}{
{`{"foo": 5}["foo"]`, 5},
{`{"foo": 5}["bar"]`, nil},
{`ask key = "foo"; {"foo": 5}[key]`, 5},
{`{}["foo"]`, nil},
{`{5: 5}[5]`, 5},
{`{fact: 5}[fact]`, 5},
{`{cap: 5}[cap]`, 5},
}

for _, tt := range tests {
t.Run(tt.input, func(t *testing.T) {
evaluated := testEval(tt.input)

integer, ok := tt.expected.(int)
if ok {
testIntegerObject(t, evaluated, int64(integer))
} else {
testNullObject(t, evaluated)
}
})
}
}
38 changes: 10 additions & 28 deletions example.mag
Original file line number Diff line number Diff line change
@@ -1,39 +1,21 @@
// Declare a mutable variable using "ask"
ask x = 10;

// Declare a constant using "tell"
tell y = 5;
ask x = 10
ask y = 1

// A basic arithmetic operation
ask z = x + y;
ask z = x + y

// Print the result
console.log('The sum of x and y is:', z);

// A range loop using "therefore"
therefore (ask i = 0; i < x; i++) {
// A conditional statement using "consider"
consider(i === z) {
console.log('i is equal to z:', i);
// exit statement
snap;
}
}
log("The sum of x and y is:", z)

// A Lord's loop using "during"
ask j = 0;
during (j < x) {
console.log('Current value of j:', j);
j++;
}
// A conditional statement using "consider"
consider(5 == z) { log("5 is equal to z:", 5) }

// A function declaration needs to be declared as a funk and call
funk multiply(a, b) {
giving a * b;
}
ask multiply = funk (a, b) { giving a * b }

// boolean values
cap;
fact;
cap
fact

console.log('Multiply 5 and 6:', multiply(5, 6));
log("Multiply 5 and 6:", multiply(5, 6))
6 changes: 6 additions & 0 deletions lexer/lexer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ func TestLexer_NextToken(t *testing.T) {
"foobar"
"foo bar"
[1, 2];
{"foo": "bar"}
`

tests := []struct {
Expand Down Expand Up @@ -118,6 +119,11 @@ func TestLexer_NextToken(t *testing.T) {
{token.INT, "2"},
{token.RBRACKET, "]"},
{token.SEMICOLON, ";"},
{token.LSQUIGGLE, "{"},
{token.STRING, "foo"},
{token.COLON, ":"},
{token.STRING, "bar"},
{token.RSQUIGGLE, "}"},
{token.EOF, ""},
}

Expand Down
4 changes: 4 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,9 @@ import (
)

func main() {
if len(os.Args) > 1 {
repl.RunFile(os.Args[1])
return
}
repl.Start(os.Stdin, os.Stdout)
}
Loading

0 comments on commit 3587000

Please sign in to comment.