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
5 changes: 4 additions & 1 deletion internal/ast/desugar_records.go
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,10 @@ func desugarRecords(node *Node) bool {
rest := rnLet(at, build(nt, keys[1:]), rnSym(nt, at), rnList(at, rnSym("map_get", at), rnSym(cur, at), rnSym(kt, at)))
return rnLet(at, rnList(at, rnSym("do", at), fill, rest), rnSym(kt, at), keys[0])
}
node.Children = rnLet(at, build(t, args[1:len(args)-1]), rnSym(t, at), args[0]).Children
// #161: put evaluates to the updated (root) dict on every path, so
// (encode_json (put d k v)) sees a value instead of an empty stack.
body := rnList(at, rnSym("do", at), build(t, args[1:len(args)-1]), rnSym(t, at))
node.Children = rnLet(at, body, rnSym(t, at), args[0]).Children
case "read_json":
node.Children = []*Node{rnSym("parse_json", at), rnSym("Any", at), rnList(at, rnSym("read_text", at), args[0])}
case "write_json":
Expand Down
22 changes: 20 additions & 2 deletions internal/vm/vm.go
Original file line number Diff line number Diff line change
Expand Up @@ -1264,6 +1264,9 @@ func interpNumericDiv(a, b any, node *ast.Node) any {
}

func interpNumericRelational(op string, a, b any, node *ast.Node) bool {
if relationalHasNaN(a, b) {
return false
}
cmp, numeric := compareNumeric(a, b)
if !numeric {
cmp, numeric = compareStrings(a, b)
Expand Down Expand Up @@ -1331,6 +1334,18 @@ func mulOverflowsInt64(a, b int64) bool {
return c/b != a
}

// relationalHasNaN reports whether a numeric relational operand pair includes
// NaN. IEEE 754 and Python order NaN against nothing, so <, <=, > and >= are
// all false (#158). Non-numeric operands still fall through to the type check.
func relationalHasNaN(a, b any) bool {
af, aOk := toFloat64(a)
bf, bOk := toFloat64(b)
if !aOk || !bOk {
return false
}
return math.IsNaN(af) || math.IsNaN(bf)
}

// compareNumeric compares two numeric values. It returns the signed comparison
// result (-1, 0, 1) and true when both operands are numeric. Mixed comparisons
// never round an exact integer through float64; they are compared as int64 when
Expand Down Expand Up @@ -3507,6 +3522,9 @@ func bcNumericBinop(op string, a, b any) any {
}
return res
case "<", ">", "<=", ">=":
if relationalHasNaN(a, b) {
return false
}
cmp, numeric := compareNumeric(a, b)
if !numeric {
cmp, numeric = compareStrings(a, b)
Expand Down Expand Up @@ -3699,9 +3717,9 @@ func normalizeForJSON(v any) any {
out[i] = normalizeForJSON(val)
}
return out
case int64:
return float64(t)
default:
// int64 stays int64 so encoding/json writes it exactly; the old
// float64 conversion rounded integers above 2^53 (#156).
return v
}
}
Expand Down
54 changes: 54 additions & 0 deletions issue161_156_158_cli_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package main

import (
"strings"
"testing"
)

// issue161Run runs src on the interpreter, governed AST bytecode and HFIR
// bytecode and requires the same exact stdout on every path.
func issue161Run(t *testing.T, src, want string) {
t.Helper()
bin := c1c2Binary(t)
for _, args := range issue149Paths(t, bin, src) {
out, err := issue151Run(t, bin, args)
if err != nil || out != want {
t.Errorf("%v: err=%v out=%q want %q", args[0], err, out, want)
}
}
}

// Issue #161: put evaluates to the updated root dict on every path, so it
// can be used as an expression (encode_json used to see null or underflow).
func TestIssue161PutReturnsDictOnEveryPath(t *testing.T) {
src := `(do (let (d (dict)) (do (print (encode_json (put d "a" 1))) (print (encode_json d))))` +
` (let (n (dict)) (do (print (encode_json (put n "a" "b" 2))) (print (encode_json (put n "a" "c" 3)))))` +
` (let (s (dict)) (do (put s "k" 5) (print (encode_json s)))))`
want := "{\"a\":1}\n{\"a\":1}\n{\"a\":{\"b\":2}}\n{\"a\":{\"b\":2,\"c\":3}}\n{\"k\":5}\n"
issue161Run(t, src, want)
}

// Issue #156: encode_json writes int64 exactly, with no float64 rounding
// above 2^53.
func TestIssue156EncodeJSONInt64ExactOnEveryPath(t *testing.T) {
src := `(do (print (encode_json 9007199254740993)) (print (encode_json (list 9223372036854775807 -9223372036854775808 9007199254740992)))` +
` (print (encode_json (dict ("big" 1234567890123456789)))) (print (encode_json 2.5)))`
want := "9007199254740993\n[9223372036854775807,-9223372036854775808,9007199254740992]\n{\"big\":1234567890123456789}\n2.5\n"
issue161Run(t, src, want)
}

// Issue #158: <, <=, > and >= with a NaN operand are false (IEEE 754,
// Python), and sort/min/max and the _by forms keep their prior output.
func TestIssue158NaNOrderingOnEveryPath(t *testing.T) {
cmp := []string{}
for _, op := range []string{"<", "<=", ">", ">="} {
for _, pair := range []string{"n 1", "1 n", "n 1.0", "1.0 n", "n n"} {
cmp = append(cmp, "("+op+" "+pair+")")
}
}
src := `(do (let (n (to_float "nan")) (do (print (str_join (list ` + strings.Join(cmp, " ") + `) " ")) (print (< 1 2.0) (<= 2 2.0) (>= 3.0 3) (> 1.5 1))` +
` (let (xs (list 3.0 n 1.0 2.0)) (do (print (sort xs)) (print (min xs)) (print (max xs))` +
` (print (sort_by x xs (* x 1.0))) (print (max_by x xs x)) (print (min_by x xs x)))))))`
want := strings.TrimSpace(strings.Repeat("false ", len(cmp))) + "\ntrue true true true\n[1 2 3 NaN]\n1\n3\n[1 2 3 NaN]\n3\n1\n"
issue161Run(t, src, want)
}
15 changes: 15 additions & 0 deletions tools/difftest/difftest_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -255,3 +255,18 @@ func TestErrorNormalization(t *testing.T) {
}
}
}

// #161: only an explicitly fenced construct may skip the HFIR leg.
func TestHFIRFencedConstructIsExplicit(t *testing.T) {
fenced := `{"reason":"HFIR direct lowering failed: node kind \"optimize_signature\" is not in the Phase-1 executable subset"}`
if got := hfirFencedConstruct(fenced); got != "optimize_signature" {
t.Fatalf("hfirFencedConstruct(optimize_signature) = %q", got)
}
other := `{"reason":"HFIR direct lowering failed: node kind \"some_new_form\" is not in the Phase-1 executable subset"}`
if got := hfirFencedConstruct(other); got != "" {
t.Fatalf("hfirFencedConstruct(some_new_form) = %q, want no skip", got)
}
if got := hfirFencedConstruct("type error"); got != "" {
t.Fatalf("hfirFencedConstruct(other error) = %q, want no skip", got)
}
}
36 changes: 28 additions & 8 deletions tools/difftest/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -120,28 +120,32 @@ func testFixture(file string) (passed bool, skipReason string, failMsg string) {
// #159: the experimental HFIR bytecode path is a third leg; any
// divergence from the interpreter fails the run.
hfirPath := filepath.Join(outDir, "test.hfir.bc")
// A construct on the locked HFIR promote-blocker fence (see
// TestProdFlipCriteriaLock) is refused at lowering, fail closed; that is
// not a semantic mismatch, so only that refusal skips the leg.
out8, hfirFenced := out1, false
// #161: only a construct on the explicit HFIR fence list (the locked
// promote-blocker fence, see TestProdFlipCriteriaLock) may skip the HFIR
// leg. Any other Phase-1 subset refusal is a failure, and a fenced fixture
// is reported as SKIP, never counted as Passed.
out8, hfirFenced := "", ""
out7, err := runCommand("go", "run", "howlframe.go", "-compile-hfir-bc", file, "-o", hfirPath)
if err != nil {
if !strings.Contains(out7, "is not in the Phase-1 executable subset") {
hfirFenced = hfirFencedConstruct(out7)
if hfirFenced == "" {
return false, "", fmt.Sprintf("(compile-hfir-bc): %v\n%s", err, out7)
}
hfirFenced = true
}
if !hfirFenced {
if hfirFenced == "" {
out8, err = runCommand("go", "run", "howlframe.go", "-run-bc", "-allow-caps", "network,filesystem,process,environment,database", hfirPath)
if err != nil {
return false, "", fmt.Sprintf("(run-bc hfir): %v\n%s", err, out8)
}
}

if out1 != out4 || out1 != out6 || out1 != out8 {
if out1 != out4 || out1 != out6 || (hfirFenced == "" && out1 != out8) {
msg := fmt.Sprintf("stdout mismatch\n--- interpreter ---\n%s\n--- go backend ---\n%s\n--- bytecode ---\n%s\n--- hfir bytecode ---\n%s", out1, out4, out6, out8)
return false, "", msg
}
if hfirFenced != "" {
return false, fmt.Sprintf("HFIR leg fenced (%s); interpreter, go and bytecode legs agree", hfirFenced), ""
}

return true, "", ""
}
Expand All @@ -158,3 +162,19 @@ func runCommand(name string, arg ...string) (string, error) {
out, err := cmd.CombinedOutput()
return string(out), err
}

// hfirFencedConstructs is the explicit list of constructs whose HFIR lowering
// refusal skips the HFIR leg. It must not grow to hide unsupported constructs.
var hfirFencedConstructs = []string{"optimize_signature"}

// hfirFencedConstruct returns the fenced construct named by an HFIR lowering
// refusal, or "" when the refusal is not for a construct on the list.
func hfirFencedConstruct(out string) string {
for _, kind := range hfirFencedConstructs {
if strings.Contains(out, `node kind \"`+kind+`\" is not in the Phase-1 executable subset`) ||
strings.Contains(out, `node kind "`+kind+`" is not in the Phase-1 executable subset`) {
return kind
}
}
return ""
}
Loading