interp: apply integer division when appropriate

When working with an untyped const expression involving a division, if
the default type of the result should be an int (for example because the
default types of all the operands are ints as well), then we should make
sure that the operation that is applied is indeed an integer division,
and that the type of the result is not a float.

This is achieved by using the QUO_ASSIGN operator, instead of the QUO
operator.

This should fix several problems lurking around, and it notably fixes
one of the visible consequences, which is a systematic panic when using
the REPL as a "calculator".

This incidentally also allows us to revert what was done in
5dfc3b86dc since it now turns out it was
just a hack to fix one of the symptoms.

Fixes #864
This commit is contained in:
mpl
2020-10-05 10:50:03 +02:00
committed by GitHub
parent c3cf301c60
commit 16f5586a11
7 changed files with 143 additions and 39 deletions

14
_test/const17.go Normal file
View File

@@ -0,0 +1,14 @@
package main
import "fmt"
var t [7/3]int
func main() {
t[0] = 3/2
t[1] = 5/2
fmt.Println(t)
}
// Output:
// [1 2]

View File

@@ -72,6 +72,9 @@ Options:
include unsafe symbols.
Debugging support (may be removed at any time):
YAEGI_PROMPT=1
Force enable the printing of the REPL prompt and the result of last instruction,
even if stdin is not a terminal.
YAEGI_AST_DOT=1
Generate and display graphviz dot of AST with dotty(1)
YAEGI_CFG_DOT=1

View File

@@ -196,6 +196,18 @@ func {{$name}}Const(n *node) {
{{- if $op.Shift}}
v := constant.Shift(vConstantValue(v0), token.{{tokenFromName $name}}, uint(vUint(v1)))
n.rval.Set(reflect.ValueOf(v))
{{- else if (eq $op.Name "/")}}
var operator token.Token
// When the result of the operation is expected to be an int (because both
// operands are ints), we want to force the type of the whole expression to be an
// int (and not a float), which is achieved by using the QUO_ASSIGN operator.
if n.typ.untyped && isInt(n.typ.rtype) {
operator = token.QUO_ASSIGN
} else {
operator = token.QUO
}
v := constant.BinaryOp(vConstantValue(v0), operator, vConstantValue(v1))
n.rval.Set(reflect.ValueOf(v))
{{- else}}
v := constant.BinaryOp(vConstantValue(v0), token.{{tokenFromName $name}}, vConstantValue(v1))
n.rval.Set(reflect.ValueOf(v))

View File

@@ -785,20 +785,28 @@ func (interp *Interpreter) REPL() (reflect.Value, error) {
}
}
func doPrompt(out io.Writer) func(v reflect.Value) {
return func(v reflect.Value) {
if v.IsValid() {
fmt.Fprintln(out, ":", v)
}
fmt.Fprint(out, "> ")
}
}
// getPrompt returns a function which prints a prompt only if input is a terminal.
func getPrompt(in io.Reader, out io.Writer) func(reflect.Value) {
forcePrompt, _ := strconv.ParseBool(os.Getenv("YAEGI_PROMPT"))
if forcePrompt {
return doPrompt(out)
}
s, ok := in.(interface{ Stat() (os.FileInfo, error) })
if !ok {
return func(reflect.Value) {}
}
stat, err := s.Stat()
if err == nil && stat.Mode()&os.ModeCharDevice != 0 {
return func(v reflect.Value) {
if v.IsValid() {
fmt.Fprintln(out, ":", v)
}
fmt.Fprint(out, "> ")
}
return doPrompt(out)
}
return func(reflect.Value) {}
}

View File

@@ -1331,3 +1331,91 @@ func applyCIMultiplier(timeout time.Duration) time.Duration {
}
return time.Duration(float64(timeout) * CITimeoutMultiplier)
}
func TestREPLDivision(t *testing.T) {
_ = os.Setenv("YAEGI_PROMPT", "1")
defer func() {
_ = os.Setenv("YAEGI_PROMPT", "0")
}()
allDone := make(chan bool)
runREPL := func() {
done := make(chan error)
pinin, poutin := io.Pipe()
pinout, poutout := io.Pipe()
i := interp.New(interp.Options{Stdin: pinin, Stdout: poutout})
i.Use(stdlib.Symbols)
go func() {
_, _ = i.REPL()
}()
defer func() {
_ = pinin.Close()
_ = poutin.Close()
_ = pinout.Close()
_ = poutout.Close()
allDone <- true
}()
input := []string{
`1/1`,
`7/3`,
`16/5`,
`3./2`, // float
}
output := []string{
`1`,
`2`,
`3`,
`1.5`,
}
go func() {
sc := bufio.NewScanner(pinout)
k := 0
for sc.Scan() {
l := sc.Text()
if l != "> : "+output[k] {
done <- fmt.Errorf("unexpected output, want %q, got %q", output[k], l)
return
}
k++
if k > 3 {
break
}
}
done <- nil
}()
for _, v := range input {
in := strings.NewReader(v + "\n")
if _, err := io.Copy(poutin, in); err != nil {
t.Fatal(err)
}
select {
case err := <-done:
if err != nil {
t.Fatal(err)
}
return
default:
time.Sleep(time.Second)
}
}
if err := <-done; err != nil {
t.Fatal(err)
}
}
go func() {
runREPL()
}()
timeout := time.NewTimer(10 * time.Second)
select {
case <-allDone:
case <-timeout.C:
t.Fatal("timeout")
}
}

View File

@@ -701,7 +701,16 @@ func quoConst(n *node) {
n.rval = reflect.New(t).Elem()
switch {
case isConst:
v := constant.BinaryOp(vConstantValue(v0), token.QUO, vConstantValue(v1))
var operator token.Token
// When the result of the operation is expected to be an int (because both
// operands are ints), we want to force the type of the whole expression to be an
// int (and not a float), which is achieved by using the QUO_ASSIGN operator.
if n.typ.untyped && isInt(n.typ.rtype) {
operator = token.QUO_ASSIGN
} else {
operator = token.QUO
}
v := constant.BinaryOp(vConstantValue(v0), operator, vConstantValue(v1))
n.rval.Set(reflect.ValueOf(v))
case isComplex(t):
n.rval.SetComplex(vComplex(v0) / vComplex(v1))

View File

@@ -1040,24 +1040,10 @@ func (check typecheck) convertUntyped(n *node, typ *itype) error {
return convErr
}
isFloatToIntDivision := false
if err := check.representable(n, rtyp); err != nil {
if !isInt(rtyp) || n.action != aQuo {
return err
}
// retry in the case of a division, and pretend we want a float. Because if we
// can represent a float, then it follows that we can represent the integer
// part of that float as an int.
if err := check.representable(n, reflect.TypeOf(1.0)); err != nil {
return err
}
isFloatToIntDivision = true
}
if isFloatToIntDivision {
n.rval, err = check.convertConstFloatToInt(n.rval)
} else {
n.rval, err = check.convertConst(n.rval, rtyp)
return err
}
n.rval, err = check.convertConst(n.rval, rtyp)
if err != nil {
if errors.Is(err, errCantConvert) {
return convErr
@@ -1099,22 +1085,6 @@ func (check typecheck) representable(n *node, t reflect.Type) error {
return nil
}
func (check typecheck) convertConstFloatToInt(v reflect.Value) (reflect.Value, error) {
if !v.IsValid() {
return v, errors.New("invalid float reflect value")
}
c, ok := v.Interface().(constant.Value)
if !ok {
return v, errors.New("unexpected non-constant value")
}
if constant.ToFloat(c).Kind() != constant.Float {
return v, errors.New("const value cannot be converted to float")
}
fl, _ := constant.Float64Val(c)
return reflect.ValueOf(int(fl)).Convert(reflect.TypeOf(1.0)), nil
}
func (check typecheck) convertConst(v reflect.Value, t reflect.Type) (reflect.Value, error) {
if !v.IsValid() {
// TODO(nick): This should be an error as the const is in the frame which is undesirable.