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
20 changes: 14 additions & 6 deletions docs/stdlib.md
Original file line number Diff line number Diff line change
Expand Up @@ -1069,14 +1069,24 @@ Import with `nab "testing"`. Provides test assertions and test execution.
- Functions named `catwalk_*` are output verification tests β€” they capture stdout and compare it to an expected string.
- Test functions must take no parameters.

`judge`, `expect` and `refuse` are builtins: they need no `nab` and work
anywhere a program is written, not only under `meow test`. An assertion that
holds answers with `catnap`. One that fails answers with a `Furball` carrying
its message, so it propagates like any other failure and `~>` can catch it;
under `meow test` that is what fails the enclosing test.

```meow
nya(judge(1 == 1, "one is one")) # => catnap
nya(judge(1 == 2, "nope") ~> "caught") # => caught
```

### `testing.judge(condition [, message])`

Assert that a condition is truthy.

- **condition**: Value to check for truthiness.
- **message** (string, optional): Custom failure message.
- **Returns**: `catnap`.
- **Panics (test failure)**: If condition is falsy.
- **Returns**: `catnap` if the condition is truthy, otherwise a `Furball` carrying the message.

```meow
judge(1 + 1 == 2)
Expand All @@ -1090,8 +1100,7 @@ Assert that two values are equal (compared by string representation).
- **actual**: The value to check.
- **expected**: The expected value.
- **message** (string, optional): Custom failure message.
- **Returns**: `catnap`.
- **Panics (test failure)**: If values are not equal.
- **Returns**: `catnap` if the values are equal, otherwise a `Furball` carrying the message.

```meow
expect(1 + 1, 2, "basic addition")
Expand All @@ -1104,8 +1113,7 @@ Assert that a condition is falsy.

- **condition**: Value to check for falsiness.
- **message** (string, optional): Custom failure message.
- **Returns**: `catnap`.
- **Panics (test failure)**: If condition is truthy.
- **Returns**: `catnap` if the condition is falsy, otherwise a `Furball` carrying the message.

```meow
refuse(1 == 2)
Expand Down
12 changes: 4 additions & 8 deletions pkg/codegen/builtinvalue.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,6 @@ import (
"github.com/135yshr/meow/pkg/builtins"
)

// seedCall stands where a runtime function would, for the one builtin that has
// none: a call to seed outside a fuzz run answers with catnap.
const seedCall = "__seed"

// builtinCalls says how to reach a builtin's runtime function when the builtin
// is named rather than called: the Go expression that invokes it.
//
Expand All @@ -30,8 +26,10 @@ var builtinCalls = map[string]string{
"hiss": "meow.Hiss",
"scram": "meow.Scram",
// seed marks a fuzz corpus entry and is read off the AST before a body is
// generated, so outside a fuzz run it is the no-op genCall also emits.
"seed": seedCall,
// generated, so outside a fuzz run it is the catnap genCall also answers
// with β€” a runtime function of its own now, so that naming seed as a value
// needs no special case here.
"seed": "meow.Seed",
"gag": "meow.Gag",
"is_furball": "meow.IsFurball",
"len": "meow.Len",
Expand Down Expand Up @@ -98,8 +96,6 @@ func (g *Generator) genBuiltinValue(name string) (string, bool) {

var body string
switch {
case call == seedCall:
body = "meow.NewNil()"
case arity == builtins.Variadic:
body = fmt.Sprintf("%s(__a...)", call)
default:
Expand Down
19 changes: 18 additions & 1 deletion pkg/codegen/codegen.go
Original file line number Diff line number Diff line change
Expand Up @@ -503,6 +503,18 @@ func (g *Generator) emitTest() string {
return b.String()
}

// ensureImport records that the generator itself needs one of Meow's own
// packages, because it is about to emit a selector into it β€” `meow_testing` for
// an assertion, and nothing else so far.
//
// That is why the package is marked used here rather than where the selector is
// written. A `nab` import is the opposite case: the program asks for the
// package and may never call into it, so usedImports keeps it out of the import
// block until markPackageUsed says a selector was emitted. Registering without
// marking is what left `judge` compiling to `meow_testing.Judge(...)` in a file
// that imported nothing β€” raw Go, `undefined: meow_testing`, under `meow run`
// while `meow test` worked (#151). Only the test wrapper named the package as
// used, and it did so by hand.
func (g *Generator) ensureImport(name string) {
if g.imports == nil {
g.imports = make(map[string]string)
Expand All @@ -512,6 +524,7 @@ func (g *Generator) ensureImport(name string) {
g.imports[name] = path
}
}
g.markPackageUsed(name)
}

func (g *Generator) needsMeowImport() bool {
Expand Down Expand Up @@ -1955,8 +1968,12 @@ func (g *Generator) genCall(e *ast.CallExpr) string {
case "refuse":
g.ensureImport("testing")
return fmt.Sprintf("meow_testing.Refuse(%s)", argStr)
// seed's arguments are arguments: meow.Seed answers with catnap and
// evaluating them is what calling it does. Answering with the catnap
// alone spliced them into nothing, so a side effect written in one
// never happened here while the interpreter ran it (#151).
case "seed":
return "meow.NewNil()"
return fmt.Sprintf("meow.Seed(%s)", argStr)
default:
if ks, ok := g.kittyDefs[ident.Name]; ok {
fieldNames := make([]string, len(ks.Fields))
Expand Down
13 changes: 7 additions & 6 deletions pkg/interpreter/interpreter.go
Original file line number Diff line number Diff line change
Expand Up @@ -693,12 +693,13 @@ var builtinTable = map[string]builtinFn{

// seed states one entry of a fuzz corpus. Only `meow test -fuzz` has
// anything to do with one β€” it reads the calls out of the test's body
// before the body is generated β€” and everywhere else codegen compiles a
// seed call to catnap. The playground cannot fuzz, so everywhere else is
// all there is here.
"seed": countsItself("seed", func(_ *Interpreter, _ []meowrt.Value) meowrt.Value {
return meowrt.NewNil()
}),
// before the body is generated β€” and everywhere else it answers with
// catnap. It is the runtime's own function rather than a catnap written
// here, so that an argument which failed is passed on the way codegen
// passes it on: this answered catnap whatever it was handed, and
// `seed(judge(false, "x")) ~> "caught"` printed catnap here and "caught"
// from a compiled program (#151).
"seed": variadic("seed", meowrt.Seed),
}

func (interp *Interpreter) dispatchBuiltin(name string, args []meowrt.Value) (meowrt.Value, bool) {
Expand Down
45 changes: 30 additions & 15 deletions pkg/interpreter/interpreter_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1867,20 +1867,35 @@ nya(g())`)
}
}

// The program that pins this rule for the compiled path prints the same thing
// here, byte for byte. #154 was the two backends agreeing with each other and
// disagreeing with the spec; the fix is worth nothing if they stop agreeing,
// and the golden file is what the compiled path is held to.
func TestTheShadowingGoldenPrintsTheSameUnderTheInterpreter(t *testing.T) {
src, err := os.ReadFile(filepath.Join("..", "..", "testdata", "binding_shadows_builtin.nyan"))
if err != nil {
t.Fatal(err)
}
want, err := os.ReadFile(filepath.Join("..", "..", "testdata", "binding_shadows_builtin.golden"))
if err != nil {
t.Fatal(err)
}
if got := runMeow(t, string(src)); got != string(want) {
t.Errorf("the interpreter prints\n%s\nthe compiled program prints\n%s", got, want)
// The golden programs written to pin a rule for the compiled path print the
// same thing here, byte for byte. Each of these rules was a place where the two
// backends disagreed; the fixes are worth nothing if they drift again, and the
// golden file is what the compiled path is held to.
//
// Only the goldens whose programs the interpreter can run belong here: most of
// the corpus reaches for `nab`, a file or a clock, and the playground has none
// of those.
func TestTheseGoldensPrintTheSameUnderTheInterpreter(t *testing.T) {
for _, name := range []string{
// A binding takes a builtin's or a constructor's name wherever it is
// written (#154).
"binding_shadows_builtin",
// judge, expect and refuse work outside `meow test`, and seed's
// arguments are evaluated (#151).
"assertions_anywhere",
} {
t.Run(name, func(t *testing.T) {
src, err := os.ReadFile(filepath.Join("..", "..", "testdata", name+".nyan"))
if err != nil {
t.Fatal(err)
}
want, err := os.ReadFile(filepath.Join("..", "..", "testdata", name+".golden"))
if err != nil {
t.Fatal(err)
}
if got := runMeow(t, string(src)); got != string(want) {
t.Errorf("the interpreter prints\n%s\nthe compiled program prints\n%s", got, want)
}
})
}
}
21 changes: 21 additions & 0 deletions runtime/meowrt/builtins.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,27 @@ import (
"strings"
)

// Seed states one entry of a fuzz corpus, and answers with catnap.
//
// Only `meow test -fuzz` has anything to do with a seed call: it reads the
// calls out of a fuzz target's body before that body is generated. Everywhere
// else the call is this, which exists so that the arguments are arguments β€”
// evaluated where they are written, the way Meow evaluates every other call's.
// Codegen used to answer a seed call with the catnap alone and splice the
// argument expressions into nothing, so a side effect written in one never
// happened, while the playground's interpreter ran it (#151).
//
// An unhandled Furball among them is passed on rather than swallowed, as Nya
// does, so a seed whose argument failed says so.
func Seed(args ...Value) Value {
for _, a := range args {
if f, ok := a.(*Furball); ok && !f.Handled {
return f
}
}
return NewNil()
}

// Nya prints a value (the Meow print function). If any argument is an
// unhandled Furball, Nya returns it without printing so the error
// propagates instead of being stringified and silently swallowed.
Expand Down
12 changes: 12 additions & 0 deletions testdata/assertions_anywhere.golden
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
catnap
catnap
catnap
caught the judge
caught the expect
caught the refuse
catnap
catnap
caught from a function
evaluated
catnap
caught the seed argument
45 changes: 45 additions & 0 deletions testdata/assertions_anywhere.nyan
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# judge, expect and refuse are ordinary builtins that answer with a Furball,
# and they work wherever a program is written β€” not only under `meow test`.
#
# They used to compile to `meow_testing.Judge(...)` in a file that never
# imported the package, so `meow run` answered with raw Go: `undefined:
# meow_testing`, naming no line of the .nyan file. The checker accepted them
# everywhere and the interpreter ran them everywhere, so the CLI was the odd one
# out (#151). What this file prints is what the interpreter prints for it, byte
# for byte.

# An assertion that holds answers with catnap, as it does under `meow test`,
# where only the enclosing test's PASS line is printed.
nya(judge(1 == 1, "one is one"))
nya(expect(2 + 2, 4, "two and two"))
nya(refuse(1 == 2, "one is not two"))

# One that fails is a Furball, so `~>` can catch it and the message is the one
# the assertion was given.
nya(judge(1 == 2, "one is not two") ~> "caught the judge")
nya(expect(2 + 2, 5, "two and two are not five") ~> "caught the expect")
nya(refuse(1 == 1, "one is one after all") ~> "caught the refuse")

# Held as a value, an assertion is still the assertion β€” the wrapper #152 gave
# every builtin needs the same import.
nyan held = judge
nya(held(1 == 1, "still one"))

# Inside a function, where the Furball propagates as any other does.
meow check(n int) furball {
bring judge(n > 0, "n must be positive")
}
nya(check(1))
nya(check(-1) ~> "caught from a function")

# seed states one entry of a fuzz corpus. Only `meow test -fuzz` has anything to
# do with one, and everywhere else it answers with catnap β€” but its arguments
# are arguments, and Meow evaluates those. Codegen used to splice them into
# nothing at all, so a side effect written in one never happened while the
# interpreter ran it.
nya(seed(nya("evaluated")))

# An argument that fails is passed on, the way nya passes one on, so a seed
# whose argument failed says so rather than answering catnap as if nothing had
# happened. Both backends call the same runtime function for this.
nya(seed(judge(1 == 2, "a seed argument failed")) ~> "caught the seed argument")
20 changes: 14 additions & 6 deletions website/content/doc/stdlib.md
Original file line number Diff line number Diff line change
Expand Up @@ -1073,14 +1073,24 @@ Import with `nab "testing"`. Provides test assertions and test execution.
- Functions named `catwalk_*` are output verification tests β€” they capture stdout and compare it to an expected string.
- Test functions must take no parameters.

`judge`, `expect` and `refuse` are builtins: they need no `nab` and work
anywhere a program is written, not only under `meow test`. An assertion that
holds answers with `catnap`. One that fails answers with a `Furball` carrying
its message, so it propagates like any other failure and `~>` can catch it;
under `meow test` that is what fails the enclosing test.

```meow
nya(judge(1 == 1, "one is one")) # => catnap
nya(judge(1 == 2, "nope") ~> "caught") # => caught
```

### `testing.judge(condition [, message])`

Assert that a condition is truthy.

- **condition**: Value to check for truthiness.
- **message** (string, optional): Custom failure message.
- **Returns**: `catnap`.
- **Panics (test failure)**: If condition is falsy.
- **Returns**: `catnap` if the condition is truthy, otherwise a `Furball` carrying the message.

```meow
judge(1 + 1 == 2)
Expand All @@ -1094,8 +1104,7 @@ Assert that two values are equal (compared by string representation).
- **actual**: The value to check.
- **expected**: The expected value.
- **message** (string, optional): Custom failure message.
- **Returns**: `catnap`.
- **Panics (test failure)**: If values are not equal.
- **Returns**: `catnap` if the values are equal, otherwise a `Furball` carrying the message.

```meow
expect(1 + 1, 2, "basic addition")
Expand All @@ -1108,8 +1117,7 @@ Assert that a condition is falsy.

- **condition**: Value to check for falsiness.
- **message** (string, optional): Custom failure message.
- **Returns**: `catnap`.
- **Panics (test failure)**: If condition is truthy.
- **Returns**: `catnap` if the condition is falsy, otherwise a `Furball` carrying the message.

```meow
refuse(1 == 2)
Expand Down
Loading