Skip to content

Commit

Permalink
feat: first commit
Browse files Browse the repository at this point in the history
  • Loading branch information
angelofallars committed Dec 7, 2023
0 parents commit e25bbbb
Show file tree
Hide file tree
Showing 12 changed files with 501 additions and 0 deletions.
46 changes: 46 additions & 0 deletions .air.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
root = "."
testdata_dir = "testdata"
tmp_dir = "tmp"

[build]
args_bin = []
bin = "./tmp/main"
cmd = "go build -o ./tmp/main ."
delay = 1000
exclude_dir = ["assets", "tmp", "vendor", "testdata"]
exclude_file = []
exclude_regex = ["_test.go"]
exclude_unchanged = false
follow_symlink = false
full_bin = ""
include_dir = []
include_ext = ["go", "tpl", "tmpl", "html"]
include_file = []
kill_delay = "0s"
log = "build-errors.log"
poll = false
poll_interval = 0
post_cmd = []
pre_cmd = []
rerun = false
rerun_delay = 500
send_interrupt = false
stop_on_error = false

[color]
app = ""
build = "yellow"
main = "magenta"
runner = "green"
watcher = "cyan"

[log]
main_only = false
time = false

[misc]
clean_on_exit = false

[screen]
clear_on_rebuild = false
keep_scroll = true
11 changes: 11 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Binaries
/bin/
__debug*

# Temporary
tmp/

# EWW
node_modules/

reference/
12 changes: 12 additions & 0 deletions cmd/hypo/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package main

import (
"os"

"github.com/angelofallars/hypo/internal/cmd"
)

func main() {
statusCode := cmd.Exec()
os.Exit(statusCode)
}
2 changes: 2 additions & 0 deletions example/helloworld.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
<s>Hello world!</s>
<output></output>
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/angelofallars/hypo

go 1.21.5

require golang.org/x/net v0.19.0

require (
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/spf13/cobra v1.8.0
github.com/spf13/pflag v1.0.5 // indirect
)
12 changes: 12 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0=
github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c=
golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
54 changes: 54 additions & 0 deletions internal/cmd/root.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package cmd

import (
"fmt"
"os"

"github.com/angelofallars/hypo/internal/evaluator"
"github.com/angelofallars/hypo/internal/object"
"github.com/angelofallars/hypo/internal/parser"
"github.com/angelofallars/hypo/internal/repl"
"github.com/spf13/cobra"
)

func Exec() int {
rootCmd := &cobra.Command{
Use: "hypo [ file ]",
Short: "Hypo is a fast runtime for HTML, the programming language running outside the browser.",
Args: cobra.MaximumNArgs(1),
Run: func(cmd *cobra.Command, args []string) {
if len(args) == 0 {
repl.Start()
}

bytes, err := os.ReadFile(args[0])
if err != nil {
cmd.PrintErrln(err)
return
}

contents := string(bytes)

node, err := parser.Parse(contents)
if err != nil {
cmd.PrintErrln(err)
return
}

env := object.NewEnv()

err = evaluator.Exec(node, env)
if err != nil {
cmd.PrintErrln(err)
return
}
},
}

if err := rootCmd.Execute(); err != nil {
fmt.Println(err)
return 1
}

return 0
}
168 changes: 168 additions & 0 deletions internal/evaluator/evaluator.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
package evaluator

import (
"errors"
"fmt"
"strconv"

"github.com/angelofallars/hypo/internal/object"
"golang.org/x/net/html"
)

type command func(node *html.Node, env *object.Env) error

type number = float64

type binOp uint

const (
add binOp = iota
subtract
multiply
divide
)

// Exec executes an HTML, the programming language, program
// from an [html.Node], traversing through it and its siblings.
func Exec(node *html.Node, env *object.Env) error {
for n := node; n != nil; n = n.NextSibling {
cmd, ok := commands[n.Data]
if !ok {
continue
}

err := cmd(n, env)
if err != nil {
fmt.Printf("error on <%v>: %v\n", n.Data, err)
}
}

return nil
}

// commands maps an HTML tag name to a command function.
var commands = map[string]command{
// Literals
"s": evalPushString,
"data": evalPushNumber,
// Math Commands
"dd": evalBinOp(add),
"sub": evalBinOp(subtract),
"ul": evalBinOp(multiply),
"div": evalBinOp(divide),
// Stack Manipulation commands
"dt": evalDuplicate,
"del": evalDelete,
// I/O
"output": evalPrintOutput,
}

// evalPushString pushes a string into the stack.
func evalPushString(node *html.Node, env *object.Env) error {
env.Stack.Push(node.FirstChild.Data)
return nil
}

// evalPushNumber pushes a number into the stack.
func evalPushNumber(node *html.Node, env *object.Env) error {
attrs := attrMap(node)

value, err := getAttr(attrs, "value")
if err != nil {
return err
}

number, err := strconv.ParseFloat(value, 64)
if err != nil {
return errors.New("value is not a valid number")
}

env.Stack.Push(number)

return nil
}

// evalBinOp performs a binary operation on the top two values of the stack.
func evalBinOp(op binOp) command {
return func(node *html.Node, env *object.Env) error {
top, err := env.Stack.Pop()
if err != nil {
return err
}
next, err := env.Stack.Pop()
if err != nil {
return err
}

topNum, ok := top.(number)
if !ok {
return errors.New("first value is not a number")
}

nextNum, ok := next.(number)
if !ok {
return errors.New("second value is not a number")
}

var result number
switch op {
case add:
result = topNum + nextNum
case subtract:
result = topNum - nextNum
case multiply:
result = topNum * nextNum
case divide:
result = topNum / nextNum
}

env.Stack.Push(result)
return nil
}
}

// evalPrintOutput prints the top value without consuming it to stdout.
func evalPrintOutput(node *html.Node, env *object.Env) error {
last, err := env.Stack.Top()
if err != nil {
return err
}

fmt.Println(last)
return nil
}

// evalDelete deletes the top value on the stack.
func evalDelete(node *html.Node, env *object.Env) error {
_, err := env.Stack.Pop()
return err
}

// evalDuplicate duplicates the top value on the stack.
func evalDuplicate(node *html.Node, env *object.Env) error {
v, err := env.Stack.Top()
if err != nil {
return err
}

env.Stack.Push(v)
return nil
}

// attrMap creates a map from the Attr slice of an [html.Node].
func attrMap(node *html.Node) map[string]string {
m := make(map[string]string)
for _, attr := range node.Attr {
m[attr.Key] = attr.Val
}
return m
}

// getAttr gets a value from the attribute map, if it exists.
func getAttr(attrs map[string]string, key string) (string, error) {
value, ok := attrs[key]
if !ok {
return "", fmt.Errorf("attribute '%v' not found", key)
}
return value, nil
}
64 changes: 64 additions & 0 deletions internal/object/env.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package object

import (
"errors"
"slices"
)

// TODO: replace any with Object

// Env is an environment of the runtime which contains runtime values.
type Env struct {
// Stack is the primary storage of values.
Stack stack
Vars map[string]any
}

type stack struct {
slice []any
}

func NewEnv() *Env {
return &Env{
Stack: stack{make([]any, 0, 256)},
Vars: map[string]any{
// Start with standard variables for common values
"true": true,
"false": false,
"null": nil,
},
}
}

var ErrStackEmpty = errors.New("Stack empty")

// Push a value to the top of the stack.
func (s *stack) Push(v any) {
s.slice = append(s.slice, v)
}

// Pop a value from the top of the stack and return it.
func (s *stack) Pop() (any, error) {
v, err := s.Top()
if err != nil {
return nil, err
}

topIndex := s.topIndex()
s.slice = slices.Delete(s.slice, topIndex, topIndex+1)
return v, nil
}

// Receive a value from the top of the stack without consuming it.
func (s *stack) Top() (any, error) {
if len(s.slice) == 0 {
return nil, ErrStackEmpty
}

return s.slice[s.topIndex()], nil
}

// topIndex returns the last index in the stack.
func (s *stack) topIndex() int {
return len(s.slice) - 1
}
Loading

0 comments on commit e25bbbb

Please sign in to comment.