Skip to content

Commit

Permalink
implement number-sequence compiler
Browse files Browse the repository at this point in the history
  • Loading branch information
wbhob committed Oct 20, 2024
1 parent f9f54c1 commit f1c62d2
Show file tree
Hide file tree
Showing 10 changed files with 518 additions and 0 deletions.
21 changes: 21 additions & 0 deletions .github/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
name: Go Tests

on:
push:
branches: [main]
pull_request:
branches: [main]

jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3

- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: "1.23.2"

- name: Run tests
run: go test -v ./... -timeout 5s
2 changes: 2 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
test:
go test -v ./... -timeout 5s
11 changes: 11 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
module github.com/wbhob/learn-compilers

go 1.23.2

require github.com/stretchr/testify v1.9.0

require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
10 changes: 10 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
61 changes: 61 additions & 0 deletions interpreter.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package main

import (
"fmt"
"strconv"
)

func runSequence(ast *ASTNode) []float64 {
// ast root is always a sequence
results := make([]float64, 0)
for _, child := range ast.Children {
switch child.Type {
case LOOP:
results = append(results, runLoop(child)...)
case VALUE:
results = append(results, runNumber(child))
case SEQUENCE:
results = append(results, runSequence(child)...)
}
}

return results
}

func runLoop(ast *ASTNode) []float64 {
element := ast.Fields["repeat"].(*ASTNode)
count := ast.Fields["count"].(*ASTNode)

if count.Type != VALUE {
panic(fmt.Sprintf("Count is not a number: %+v", count))
}

countNum, err := strconv.Atoi(count.Fields["value"].(string))
if err != nil {
panic(fmt.Sprintf("Failed to parse count: %s", count.Fields["value"].(string)))
}

results := make([]float64, 0)
for i := 0; i < countNum; i++ {
switch element.Type {
case VALUE:
results = append(results, runNumber(element))
case SEQUENCE:
results = append(results, runSequence(element)...)
default:
panic(fmt.Sprintf("Unknown element type: %s", element.Type))
}
}

return results
}

func runNumber(ast *ASTNode) float64 {
str := ast.Fields["value"].(string)
num, err := strconv.ParseFloat(str, 64)
if err != nil {
panic(fmt.Sprintf("Failed to parse number: %s", str))
}

return num
}
81 changes: 81 additions & 0 deletions lexer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package main

import (
"unicode"
)

// Token represents a lexical unit in our language
type Token struct {
Type TokenType // The type of the token
Value string // The string value of the token
}

// TokenType is a string that represents the category of a token
type TokenType string

// These constants define the possible types of tokens in our language
const (
NUMBER TokenType = "NUMBER" // Represents numeric values
COMMA TokenType = "COMMA" // Represents the comma separator
X TokenType = "X" // Represents the 'x' used for repetition
LPAREN TokenType = "LPAREN" // Represents a left parenthesis
RPAREN TokenType = "RPAREN" // Represents a right parenthesis
EOF TokenType = "EOF" // Represents the end of the input
)

// lex function takes a string input and returns a slice of Tokens
// It breaks down the input string into individual lexical units (tokens)
func lex(input string) []Token {
tokens := []Token{}
digits := ""

// Iterate through each character in the input string
for _, char := range input {
// Skip whitespace characters
if unicode.IsSpace(char) {
// If we've been building a number, add it as a token
if digits != "" {
tokens = append(tokens, Token{Type: NUMBER, Value: digits})
digits = ""
}
continue
}

// Handle characters that could be part of a number
if char == '.' || char == '-' || unicode.IsDigit(char) {
digits += string(char)
continue
}

// If we've been building a number, add it as a token
if digits != "" {
tokens = append(tokens, Token{Type: NUMBER, Value: digits})
digits = ""
}

// Handle special characters
switch char {
case ',':
tokens = append(tokens, Token{Type: COMMA, Value: ","})
case 'x':
tokens = append(tokens, Token{Type: X, Value: "x"})
case '(':
tokens = append(tokens, Token{Type: LPAREN, Value: "("})
case ')':
tokens = append(tokens, Token{Type: RPAREN, Value: ")"})
default:
// If we encounter an unknown character, panic
panic("Unknown character: " + string(char))
}
}

// If we've been building a number, add it as a token
if digits != "" {
tokens = append(tokens, Token{Type: NUMBER, Value: digits})
}

// Add an EOF token to signify the end of the input
tokens = append(tokens, Token{Type: EOF, Value: ""})

return tokens
}
17 changes: 17 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package main

import "fmt"

func ParseNumberSequenceShorthand(input string) []float64 {
fmt.Print("==========================================\n")
fmt.Printf("Parsing number sequence shorthand: %s\n", input)
tokens := lex(input)
ast := parseSequence(tokens)
return runSequence(ast)
}

func ValidateNumberSequenceShorthand(input string) error {
tokens := lex(input)
parseSequence(tokens)
return nil
}
86 changes: 86 additions & 0 deletions main_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package main_test

import (
"testing"

"github.com/stretchr/testify/assert"
main "github.com/wbhob/learn-compilers"
)

func TestParseNumberSequenceShorthand(t *testing.T) {
t.Run("Basic Functionality", func(t *testing.T) {
t.Run("handles repetition shorthand", func(t *testing.T) {
assert.Equal(t, []float64{42, 42, 42}, main.ParseNumberSequenceShorthand("42x3"))
assert.Equal(t, []float64{0, 2, 2, 2, 10, 42, 42, 42}, main.ParseNumberSequenceShorthand("0, 2x3, 10, 42x3"))
assert.Equal(t, []float64{0.1, 0.1, 2.3, 2.3, -4.5, -4.5}, main.ParseNumberSequenceShorthand(".1x2, 2.3x2, -4.5x2"))
})

t.Run("handles a group", func(t *testing.T) {
assert.Equal(t, []float64{1, 2, 3, 1, 2, 3}, main.ParseNumberSequenceShorthand("(1, 2, 3)x2"))
})

t.Run("handles a nested group", func(t *testing.T) {
assert.Equal(t, []float64{1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2}, main.ParseNumberSequenceShorthand("(((1, 2)x2)x2)x2"))
})

t.Run("complex example", func(t *testing.T) {
expected := []float64{1, 2, 1, 2, 42, 42, 42, 0.5, 1.5, 1.5, 8, 16, 8, 16, 8, 16, 0.5, 1.5, 1.5, 8, 16, 8, 16, 8, 16, 5.5}
assert.Equal(t, expected, main.ParseNumberSequenceShorthand("(1, 2)x2, 42x3, (.5, 1.5x2, (8, 16)x3)x2, 5.5"))
})
})

t.Run("Specific Features", func(t *testing.T) {
t.Run("handles single number", func(t *testing.T) {
assert.Equal(t, []float64{42}, main.ParseNumberSequenceShorthand("42"))
})

t.Run("handles multiple numbers", func(t *testing.T) {
assert.Equal(t, []float64{1, 2, 3, 4, 5, 6}, main.ParseNumberSequenceShorthand("1, 2, 3, 4, 5, 6"))
})

t.Run("handles multiple numbers (strange whitespace)", func(t *testing.T) {
assert.Equal(t, []float64{1, 2, 3, 4, 5, 6}, main.ParseNumberSequenceShorthand(" 1,2, 3 , 4 ,5, 6 "))
})

t.Run("handles decimal values", func(t *testing.T) {
assert.Equal(t, []float64{0.1, 0.23, 0.45, 6.7}, main.ParseNumberSequenceShorthand(".1, .23, 0.45, 6.7"))
})

t.Run("handles negative values", func(t *testing.T) {
assert.Equal(t, []float64{-42, -0.1, -0.25, -3.33}, main.ParseNumberSequenceShorthand("-42, -.1, -0.25, -3.33"))
})

t.Run("handles repetition shorthand, with 0 repetitions", func(t *testing.T) {
assert.Equal(t, []float64{}, main.ParseNumberSequenceShorthand("42x0"))
assert.Equal(t, []float64{1, 2, 3}, main.ParseNumberSequenceShorthand("1, 2, 42x0, 3"))
})

t.Run("handles a group with a single value", func(t *testing.T) {
assert.Equal(t, []float64{1, 1, 1}, main.ParseNumberSequenceShorthand("(1)x3"))
})

t.Run("handles mixed values with a group", func(t *testing.T) {
assert.Equal(t, []float64{0, 1, 1, 2, 3, 4, 4, 2, 3, 4, 4}, main.ParseNumberSequenceShorthand("0, 1x2, (2, 3, 4x2)x2"))
})
})

t.Run("Validation", func(t *testing.T) {
t.Run("failure cases", func(t *testing.T) {
invalidInputs := []string{
"1 2",
"(((1 2)x2)x2)x2",
"1,,2",
"(1, 2, 3)x, (4, 5)x2",
"(1, 2, 3)2, (4, 5)x2",
"(((1, 2x2)x2)x2",
"((1, 2)x2)x2)x2",
}

for _, input := range invalidInputs {
t.Run(input, func(t *testing.T) {
assert.Panics(t, func() { main.ValidateNumberSequenceShorthand(input) })
})
}
})
})
}
Loading

0 comments on commit f1c62d2

Please sign in to comment.