Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions ast/node.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,14 @@ type WhereExpr struct {
Val Expr
}

type ImportExpr struct {
Pos token.Span
// Typically "sha256".
HashAlgo string
// Any literal, typically a byte-string.
Value Literal
}

func (b Ident) expr() {}
func (b Literal) expr() {}
func (b BinaryExpr) expr() {}
Expand All @@ -95,6 +103,7 @@ func (b RecordExpr) expr() {}
func (b AccessExpr) expr() {}
func (b ListExpr) expr() {}
func (b WhereExpr) expr() {}
func (b ImportExpr) expr() {}

func span(start, end Expr) token.Span {
return token.Span{
Expand Down Expand Up @@ -122,3 +131,4 @@ func (b RecordExpr) Span() token.Span { return b.Pos }
func (b AccessExpr) Span() token.Span { return b.Pos }
func (b ListExpr) Span() token.Span { return b.Pos }
func (b *WhereExpr) Span() token.Span { return span(b.Expr, b.Val) }
func (b ImportExpr) Span() token.Span { return b.Pos }
33 changes: 15 additions & 18 deletions cmd/scrap/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,6 @@ import (

"github.com/Victorystick/scrapscript"
"github.com/Victorystick/scrapscript/eval"
"github.com/Victorystick/scrapscript/parser"
"github.com/Victorystick/scrapscript/token"
"github.com/Victorystick/scrapscript/types"
"github.com/Victorystick/scrapscript/yards"
)

Expand Down Expand Up @@ -44,20 +41,26 @@ func must[T any](val T, err error) T {
return val
}

func evaluate(args []string) {
fetcher := must(yards.NewDefaultCacheFetcher(
func makeEnv() *eval.Environment {
env := eval.NewEnvironment()
env.UseFetcher(must(yards.NewDefaultCacheFetcher(
// Don't cache invalid scraps, but trust the local cache for now.
yards.Validate(
// TODO: make configurable
yards.ByHttp("https://scraps.oseg.dev/")),
))
)))
return env
}

func evaluate(args []string) {
input := must(io.ReadAll(os.Stdin))
env := eval.NewEnvironment(fetcher)
val := must(env.Eval(input))
env := makeEnv()
scrap := must(env.Read(input))
val := must(env.Eval(scrap))

if len(args) >= 2 && args[0] == "apply" {
fn := must(env.Eval([]byte(args[1])))
scrap = must(env.Read([]byte(args[1])))
fn := must(env.Eval(scrap))
val = must(scrapscript.Call(fn, val))
}

Expand All @@ -66,13 +69,7 @@ func evaluate(args []string) {

func inferType(args []string) {
input := must(io.ReadAll(os.Stdin))
source := token.NewSource(input)

se := must(parser.Parse(&source))
str, err := types.Infer(se)
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
fmt.Println(str)
env := makeEnv()
scrap := must(env.Read(input))
fmt.Println(must(env.Infer(scrap)))
}
139 changes: 87 additions & 52 deletions eval/env.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,86 +2,121 @@ package eval

import (
"crypto/sha256"
"encoding/base64"
"encoding/hex"
"fmt"

"github.com/Victorystick/scrapscript/ast"
"github.com/Victorystick/scrapscript/parser"
"github.com/Victorystick/scrapscript/token"
"github.com/Victorystick/scrapscript/types"
"github.com/Victorystick/scrapscript/yards"
)

type Scrap struct {
expr ast.SourceExpr
typ types.TypeRef
value Value
}

type Sha256Hash = [32]byte

type Environment struct {
reg types.Registry
vars Variables
fetcher yards.Fetcher
reg types.Registry
vars Variables
scraps map[Sha256Hash]*Scrap
evalImport EvalImport
inferImport types.InferImport
}

func NewEnvironment(fetcher yards.Fetcher) *Environment {
func NewEnvironment() *Environment {
env := &Environment{}
env.vars = bindBuiltIns(&env.reg)

if fetcher != nil {
// TODO: Don't inline this. :/
env.vars["$sha256"] = BuiltInFunc{
name: "$sha256",
// We must special-case import functions, since their type is dependent
// on their returned value.
typ: env.reg.Func(types.BytesRef, types.NeverRef),
fn: func(v Value) (Value, error) {
bs, ok := v.(Bytes)
if !ok {
return nil, fmt.Errorf("cannot import non-bytes %s", v)
}

// Must convert from `eval.Byte` to `[]byte`.
hash := []byte(bs)

// Funnily enough; any lower-cased, hex-encoded sha256 hash can be parsed
// as base64. Users reading the official documentation at
// https://scrapscript.org/guide may be frustrated if this doesn't work.
// We detect this and convert back via base64 to the original hex string.
var err error
if len(hash) == sha256AsBase64Size {
hash, err = rescueSha256FromBase64(hash)
if err != nil {
return nil, err
}
}

if len(hash) != sha256.Size {
return nil, fmt.Errorf("cannot import sha256 bytes of length %d, must be %d", len(hash), sha256.Size)
}

key := fmt.Sprintf("%x", hash)
bytes, err := fetcher.FetchSha256(key)
if err != nil {
return nil, err
}

return env.Eval(bytes)
},
env.scraps = make(map[Sha256Hash]*Scrap)
env.evalImport = func(algo string, hash []byte) (Value, error) {
scrap, err := env.fetch(algo, hash)
if err != nil {
return nil, err
}
return env.Eval(scrap)
}
env.inferImport = func(algo string, hash []byte) (types.TypeRef, error) {
scrap, err := env.fetch(algo, hash)
if err != nil {
return types.NeverRef, err
}
return env.infer(scrap)
}

return env
}

const sha256AsBase64Size = 48
func (e *Environment) UseFetcher(fetcher yards.Fetcher) {
e.fetcher = fetcher
}

func (e *Environment) fetch(algo string, hash []byte) (*Scrap, error) {
if algo != "sha256" {
return nil, fmt.Errorf("only sha256 imports are supported")
}

if len(hash) != sha256.Size {
return nil, fmt.Errorf("cannot import sha256 bytes of length %d, must be %d", len(hash), sha256.Size)
}

if scrap, ok := e.scraps[(Sha256Hash)(hash)]; ok {
return scrap, nil
}

func rescueSha256FromBase64(encoded []byte) ([]byte, error) {
return hex.DecodeString(base64.StdEncoding.EncodeToString(encoded))
if e.fetcher == nil {
return nil, fmt.Errorf("cannot import without a fetcher")
}

key := fmt.Sprintf("%x", hash)
bytes, err := e.fetcher.FetchSha256(key)
if err != nil {
return nil, err
}

return e.Read(bytes)
}

func (e *Environment) Eval(script []byte) (Value, error) {
func (e *Environment) Read(script []byte) (*Scrap, error) {
src := token.NewSource(script)
se, err := parser.Parse(&src)

if err != nil {
return nil, fmt.Errorf("parse error: %w", err)
}

return Eval(se, &e.reg, e.vars)
scrap := &Scrap{expr: se}
e.scraps[sha256.Sum256(script)] = scrap
return scrap, nil
}

// Eval evaluates a Scrap.
func (e *Environment) Eval(scrap *Scrap) (Value, error) {
if scrap.value == nil {
value, err := Eval(scrap.expr, &e.reg, e.vars, e.evalImport)
scrap.value = value
return value, err
}
return scrap.value, nil
}

func (e *Environment) infer(scrap *Scrap) (types.TypeRef, error) {
if scrap.typ == types.NeverRef {
// TODO: Add a complete type scope.
scope := types.DefaultScope(&e.reg)
ref, err := types.Infer(&e.reg, scope, scrap.expr, e.inferImport)
scrap.typ = ref
return ref, err
}
return scrap.typ, nil
}

// Infer returns the string representation of the type of a Scrap.
func (e *Environment) Infer(scrap *Scrap) (string, error) {
ref, err := e.infer(scrap)
return e.reg.String(ref), err
}

// Scrap renders a Value as self-contained scrapscript program.
Expand Down
24 changes: 17 additions & 7 deletions eval/eval.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package eval

import (
"encoding/base64"
"encoding/hex"
"fmt"
"maps"
"reflect"
Expand All @@ -14,11 +15,14 @@ import (
"github.com/Victorystick/scrapscript/types"
)

type EvalImport func(algo string, hash []byte) (Value, error)

type context struct {
source *token.Source
reg *types.Registry
vars Vars
parent *context
source *token.Source
reg *types.Registry
vars Vars
evalImport EvalImport
parent *context
}

type Vars interface {
Expand Down Expand Up @@ -64,16 +68,16 @@ func (c *context) name(id *ast.Ident) string {
}

func (c *context) sub(vars Vars) *context {
return &context{c.source, c.reg, vars, c}
return &context{c.source, c.reg, vars, c.evalImport, c}
}

func (c *context) error(span token.Span, msg string) error {
return c.source.Error(span, msg)
}

// Eval evaluates a SourceExpr in the context of a set of variables.
func Eval(se ast.SourceExpr, reg *types.Registry, vars Vars) (Value, error) {
ctx := &context{&se.Source, reg, vars, nil}
func Eval(se ast.SourceExpr, reg *types.Registry, vars Vars, evalImport EvalImport) (Value, error) {
ctx := &context{&se.Source, reg, vars, evalImport, nil}

return ctx.eval(se.Expr)
}
Expand Down Expand Up @@ -102,6 +106,12 @@ func (c *context) eval(x ast.Node) (Value, error) {
return c.createMatchFunc(x)
case *ast.AccessExpr:
return c.access(x)
case *ast.ImportExpr:
bs, err := hex.DecodeString(c.source.GetString(x.Value.Pos.TrimStart(2)))
if err != nil {
return nil, c.error(x.Span(), fmt.Sprintf("bad import hash %#v", x))
}
return c.evalImport(x.HashAlgo, bs)
}

return nil, c.error(x.Span(), fmt.Sprintf("unhandled node %#v", x))
Expand Down
17 changes: 13 additions & 4 deletions eval/eval_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -162,11 +162,19 @@ func TestFailures(t *testing.T) {
}
}

func eval(e *Environment, source string) (Value, error) {
scrap, err := e.Read([]byte(source))
if err != nil {
return nil, err
}
return e.Eval(scrap)
}

// Evaluates an expression and compares the string representation of the
// result with a target string; optionally with some additional variables
// in scope.
func evalString(t *testing.T, source, expected string) {
val, err := NewEnvironment(nil).Eval([]byte(source))
val, err := eval(NewEnvironment(), source)

if err != nil {
t.Error(err)
Expand All @@ -179,7 +187,7 @@ func evalString(t *testing.T, source, expected string) {

// Evaluates to a comparable value
func evalFailure(t *testing.T, source string, expected string) {
val, err := NewEnvironment(nil).Eval([]byte(source))
val, err := eval(NewEnvironment(), source)

if err == nil {
t.Errorf("%s - should fail but got %s", source, val)
Expand All @@ -191,12 +199,13 @@ func evalFailure(t *testing.T, source string, expected string) {
}

func TestEvalImport(t *testing.T) {
env := NewEnvironment(MapFetcher{
env := NewEnvironment()
env.UseFetcher(MapFetcher{
"a948904f2f0f479b8f8197694b30184b0d2ed1c1cd2a1ec0fb85d299a192a447": `3 + $sha256~~a948904f2f0f479b8f8197694b30184b0d2ed1c1cd2a1ec0fb85d299a192a445`,
"a948904f2f0f479b8f8197694b30184b0d2ed1c1cd2a1ec0fb85d299a192a445": `2`,
})

val, err := env.Eval([]byte(`$sha256~~a948904f2f0f479b8f8197694b30184b0d2ed1c1cd2a1ec0fb85d299a192a447 - 1`))
val, err := eval(env, `$sha256~~a948904f2f0f479b8f8197694b30184b0d2ed1c1cd2a1ec0fb85d299a192a447 - 1`)
if err != nil {
t.Error(err)
} else {
Expand Down
Loading
Loading