Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

RFC - format implementation #32

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
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
6 changes: 3 additions & 3 deletions lisp/builtins.go
Original file line number Diff line number Diff line change
Expand Up @@ -2289,10 +2289,10 @@ func builtinFormatString(env *LEnv, args *LVal) *LVal {
p = strings.Join(strings.Fields(p), "")
// TODO: Allow non-empty formatting directives
if p != "{}" {
return env.Errorf("formatting direcives must be empty")
return env.Errorf("formatting directives must be empty")
}
if anonIndex >= len(fvals) {
return env.Errorf("too many formatting direcives for supplied values")
return env.Errorf("too many formatting directives for supplied values")
}
val := fvals[anonIndex]
if val.Type == LString && !val.Quoted {
Expand Down Expand Up @@ -2321,7 +2321,7 @@ func parseFormatString(f string) ([]string, error) {
}
if tok.typ == formatClose {
if len(tokens) == 0 || tokens[0].typ != formatClose {
return nil, fmt.Errorf("unexpected closing brace '}' outside of formatting direcive")
return nil, fmt.Errorf("unexpected closing brace '}' outside of formatting directive")
}
s = append(s, "}")
tokens = tokens[2:]
Expand Down
185 changes: 185 additions & 0 deletions lisp/format_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
package lisp

import (
"fmt"
"regexp"
"strconv"
"strings"
"testing"

"github.com/stretchr/testify/assert"
)

func TestFormatStringSimple(t *testing.T) {
env := NewEnv(nil)
format := String("Swap a symbol here {}")
arg1 := String("yeah")
args := &LVal{Cells: []*LVal{format, arg1}}
formatted := builtinFormatString(env, args)
assert.Equal(t, "Swap a symbol here yeah", formatted.Str)
}

func TestFormatStringComplex(t *testing.T) {
env := NewEnv(nil)
format := String("Swap a symbol here ")
built := format.Str
args := []*LVal{format}
for x := 0; x < 100; x++ {
args[0] = String(fmt.Sprintf("%s {}, blah blah,", args[0].Str))
built = fmt.Sprintf("%s %d, blah blah,", built, x)
args = append(args, String(strconv.Itoa(x)))
formatted := builtinFormatString(env, &LVal{Cells: args})
assert.Equal(t, built, formatted.Str)
}
}

func TestFormatStringUnmatched(t *testing.T) {
env := NewEnv(nil)
format := String("Swap a symbol here }")
arg1 := String("yeah")
args := &LVal{Cells: []*LVal{format, arg1}}
formatted := builtinFormatString(env, args)
assert.Equal(t, LError, formatted.Type)
}

func TestTokenizeFormatString(t *testing.T) {
x := tokenizeFormatString("This is a formatted {} string yeah {}.")
assert.Equal(t, []formatToken{
{typ: formatText, text: "This is a formatted "},
{typ: formatOpen, text: "{"},
{typ: formatClose, text: "}"},
{typ: formatText, text: " string yeah "},
{typ: formatOpen, text: "{"},
{typ: formatClose, text: "}"},
{typ: formatText, text: "."},
}, x)
}

var formatTokenRegexp = regexp.MustCompile(`\{(\d+)\}`)

// var formatBadTokenPattern = regexp.MustCompile(`(?:\{[^\d\}]|[^\{\d]\})`) // This is slooooow...
// var formatBadTokenPattern = regexp.MustCompile(`\{[^\d\}]`) // This is also slow enough that I'd ditch it

func quickformatBuiltin(env *LEnv, args *LVal) *LVal {
format := args.Cells[0]
//if formatBadTokenPattern.MatchString(format.Str) {
//return env.Errorf("unmatched token")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think there should be a test/error for unmatched tokens. The existing format-string does this.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah. Maybe this was trying to catch unclosed curly braces? What I meant was that format-string will report an error if you don't supply enough values.

elps> (format-string "hello {}, it's {} to meet you" "world")
stdin:1: lisp:format-string: too many formatting direcives for supplied values
Stack Trace [1 frames -- entrypoint last]:
  height 0: stdin:1: lisp:format-string

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should do that - it's just fmt.Sprintf inside, so it'll handle it the same way that Go does with a warning in the output.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right. fmt just outputs junk when there's a mismatch between argument and substitutions. But I don't think that is very good behavior for a language like lisp. I think a runtime error is preferable.

The annoying thing about the fmt package is that format argument mismatches can literally go unnoticed for onths or years. I think they chose to do that instead of forcing programmers to check errors for every formatting call. I've never really seen another argument for why formatting should work that way (though I guess I haven't looked hard if you had one).

In dynamic languages like python and ruby format argument mismatches cause runtime exceptions. Even if it's not completely preferable, I think it's more expected of a language like elps.

//}
replacements := make([]interface{}, len(args.Cells)-1)
for k, v := range args.Cells[1:] {
if v.Type == LString && !v.Quoted {
replacements[k] = v.Str
} else {
replacements[k] = v.String()
}
}
if format.Type != LString {
return env.Errorf("first argument is not a string")
}
pattern := formatTokenRegexp.ReplaceAllString(strings.ReplaceAll(strings.ReplaceAll(format.Str, "%", "%%"), "{}", "%s"), "%[$1]s")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My brain hurts a bit trying to parse this. This also doesn't catch syntax errors at all (e.g. unclosed curly brace).

return String(fmt.Sprintf(pattern, replacements...))
}

func TestQuickFormatStringSimple(t *testing.T) {
env := NewEnv(nil)
format := String("Swap a symbol here {}")
arg1 := String("yeah")
args := &LVal{Cells: []*LVal{format, arg1}}
formatted := quickformatBuiltin(env, args)
assert.Equal(t, "Swap a symbol here yeah", formatted.Str)
}

func TestQuickFormatStringComplex(t *testing.T) {
env := NewEnv(nil)
format := String("Swap a symbol here ")
built := format.Str
args := []*LVal{format}
for x := 0; x < 100; x++ {
args[0] = String(fmt.Sprintf("%s {}, blah blah,", args[0].Str))
built = fmt.Sprintf("%s %d, blah blah,", built, x)
args = append(args, String(strconv.Itoa(x)))
formatted := quickformatBuiltin(env, &LVal{Cells: args})
assert.Equal(t, built, formatted.Str)
}
}

func TestQuickFormatStringUnmatched(t *testing.T) {
env := NewEnv(nil)
format := String("Swap a symbol here }")
arg1 := String("yeah")
args := &LVal{Cells: []*LVal{format, arg1}}
formatted := quickformatBuiltin(env, args)
assert.Equal(t, LError, formatted.Type)
}

func TestQuickFormatStringPositional(t *testing.T) {
env := NewEnv(nil)
format := String("Swap a symbol here {2} and here {1}")
arg1 := String("yeah")
arg2 := String("oh")
args := &LVal{Cells: []*LVal{format, arg1, arg2}}
formatted := quickformatBuiltin(env, args)
assert.Equal(t, "Swap a symbol here oh and here yeah", formatted.Str)
}

func BenchmarkQuickFormatString(t *testing.B) {
env := NewEnv(nil)
format := String("Swap a symbol here {} and {} and {}")
for x := 0; x < t.N; x++ {
args := []*LVal{format, String("A"), String("B"), String("C")}
formatted := quickformatBuiltin(env, &LVal{Cells: args})
assert.Equal(t, "Swap a symbol here A and B and C", formatted.Str)
}
}

func BenchmarkQuickFormatStringWithPositionalToken(t *testing.B) {
env := NewEnv(nil)
format := String("Swap a symbol here {} and {} and {1}")
for x := 0; x < t.N; x++ {
args := []*LVal{format, String("A"), String("B")}
formatted := quickformatBuiltin(env, &LVal{Cells: args})
assert.Equal(t, "Swap a symbol here A and B and A", formatted.Str)
}
}

func BenchmarkQuickFormatStringWithATonOfTokens(t *testing.B) {
env := NewEnv(nil)
format := String("Swap a symbol here ")
built := format.Str
args := []*LVal{format}
for x := 0; x < 100; x++ {
args[0] = String(fmt.Sprintf("%s {}, blah blah,", args[0].Str))
built = fmt.Sprintf("%s %d, blah blah,", built, x)
args = append(args, String(strconv.Itoa(x)))
}
for x := 0; x < t.N; x++ {
formatted := quickformatBuiltin(env, &LVal{Cells: args})
assert.Equal(t, built, formatted.Str)
}
}

func BenchmarkFormatString(t *testing.B) {
env := NewEnv(nil)
format := String("Swap a symbol here {} and {} and {}")
for x := 0; x < t.N; x++ {
args := []*LVal{format, String("A"), String("B"), String("C")}
formatted := builtinFormatString(env, &LVal{Cells: args})
assert.Equal(t, "Swap a symbol here A and B and C", formatted.Str)
}
}

func BenchmarkFormatStringWithATonOfTokens(t *testing.B) {
env := NewEnv(nil)
format := String("Swap a symbol here ")
built := format.Str
args := []*LVal{format}
for x := 0; x < 100; x++ {
args[0] = String(fmt.Sprintf("%s {}, blah blah,", args[0].Str))
built = fmt.Sprintf("%s %d, blah blah,", built, x)
args = append(args, String(strconv.Itoa(x)))
}
for x := 0; x < t.N; x++ {
formatted := builtinFormatString(env, &LVal{Cells: args})
assert.Equal(t, built, formatted.Str)
}
}