json

A pure-Dawn JSON validator, run against JSONTestSuite's 318 fixtures.

examples/projects/json/src/main.dawn

# A pure-Dawn JSON validator, run against JSONTestSuite's 318 fixtures.
#
# Reads the file named by the first argument and prints "valid" or "invalid",
# which is the shape the JSONTestSuite harness checks.
#
# `--suite <dir>` instead runs every fixture in `dir` in this one process; see
# suite.dawn for why the gate lives here rather than in a shell loop.
#
# The parser/renderer are packages/json, consumed through [deps]. This directory
# used to carry its own copy under src/json/ and the two drifted — the package
# learned exact integer parsing, the copy did not — which meant the suite was
# certifying code no production caller used.
use std/io
use json/parser.{parse}
use json/render.{render}
use json/value.{error_text}
use suite

fn check(text: String) -> String =
  match parse(text) {
    Ok(_) -> "valid"
    Err(_) -> "invalid"
  }

fn round_trip(text: String) -> String =
  match parse(text) {
    Ok(v) -> render(v)
    Err(e) -> "ERR: " ++ error_text(e)
  }

pub fn main() -> Unit !io =
  match get(args(), 0) {
    None -> println("invalid")
    Some(path) ->
      if path == "--suite" {
        let dir = unwrap_or(get(args(), 1), "suite/test_parsing")
        let t = suite.run(dir)
        # Dawn has no exit builtin, so a nonzero exit status is a panic. The
        # summary is already printed, so this only has to be non-Ok.
        if t.fail > 0 { panic("JSONTestSuite: " ++ to_string(t.fail) ++ " mandatory case(s) failed") } else { () }
      } else {
        match io.read_file(path) {
          Ok(text) -> println(check(text))
          Err(_) -> println("invalid")
        }
      }
  }

test "parses scalars, arrays, and objects" {
  assert check("123") == "valid"
  assert check("[1, 2, 3]") == "valid"
  assert check("{\"a\": 1, \"b\": [true, null]}") == "valid"
  assert check("\"hi\"") == "valid"
}

test "rejects malformed input without panicking" {
  assert check("") == "invalid"
  assert check("[1, 2,]") == "invalid"
  assert check("{\"a\": }") == "invalid"
  assert check("01") == "invalid"
  assert check("truex") == "invalid"
}

test "round-trips through render" {
  assert round_trip("{\"n\": 1.5, \"xs\": [true, false, null]}") == "{\"n\":1.5,\"xs\":[true,false,null]}"
}

examples/projects/json/src/suite.dawn

# The JSONTestSuite harness, in Dawn, in-process.
#
# Why it exists: the repo has tracked these 318 fixtures from the start and README
# claims all of them pass, but nothing ran them. "318/318" was a result someone
# obtained once by hand, not a gate — and a claim nothing re-checks is how the
# renderer came to emit raw 0x08 for a parsed `\b` while every test stayed green.
#
# Why not a shell loop over `dawn run`: 318 JVM starts is minutes. One process
# that reads the directory itself is seconds, which is the difference between a
# check that runs on every push and one that does not.
#
# The naming convention is JSONTestSuite's (see suite/README.md):
#   y_  MUST be accepted      n_  MUST be rejected      i_  implementation-defined
# Only y_/n_ are pass/fail here. i_ files are counted and their verdict printed,
# so an intentional change (say, refusing integers wider than 64 bits) shows up
# in the diff as a number rather than passing unnoticed.
use std/io
use std/str
use std/list
use json/parser.{parse}
use json/render.{render}
use json/value.{error_text}

pub type Tally = { pass: Int, fail: Int, impl_ok: Int, impl_no: Int }

fn add(t: Tally, u: Tally) -> Tally =
  Tally {
    pass: t.pass + u.pass,
    fail: t.fail + u.fail,
    impl_ok: t.impl_ok + u.impl_ok,
    impl_no: t.impl_no + u.impl_no,
  }

# A y_ document must survive parse → render → parse with the same value. This is
# the property the fixtures alone do not check, and it is what catches a renderer
# that emits something its own parser will not read back — exactly the JSON-03
# control-character bug, which no accept/reject verdict could ever have seen.
fn round_trips(text: String) -> Result[Unit, String] =
  match parse(text) {
    Err(e) -> Err("parse: " ++ error_text(e))
    Ok(v) ->
      match parse(render(v)) {
        Err(e) -> Err("re-parse of rendered output: " ++ error_text(e))
        Ok(v2) -> if v == v2 { Ok(()) } else { Err("render changed the value") }
      }
  }

# A verdict on one document: `Ok(text)` means accepted (and `text` is what was
# decoded, so the round-trip can run), `Err(why)` means rejected and why.
#
# 25 of the fixtures are deliberately not valid UTF-8 — lone continuation bytes,
# overlong sequences, UTF-16 without a BOM, ISO-8859-1. Dawn's parser takes a
# String, so those never reach it: `io.read_file` decodes strictly and fails
# first. That is a rejection at the decode boundary, not a harness defect, and
# it is the same verdict `main` gives on those files (`Err(_) -> "invalid"`).
# All of them are n_ or i_; no y_ fixture is invalid UTF-8, which is what makes
# "decode, then parse" a faithful reading of the suite for a String-based API.
fn verdict(dir: String, name: String) -> Result[String, String] !io =
  match io.read_file(dir ++ "/" ++ name) {
    Err(e) -> Err("not UTF-8: " ++ e.message)
    Ok(text) ->
      match parse(text) {
        Err(e) -> Err(error_text(e))
        Ok(_) -> Ok(text)
      }
  }

fn check_one(dir: String, name: String) -> Tally !io = {
  let v = verdict(dir, name)
  if str.starts_with(name, "y_") {
    match v {
      Err(e) -> {
        println("FAIL " ++ name ++ ": must accept, got " ++ e)
        Tally { pass: 0, fail: 1, impl_ok: 0, impl_no: 0 }
      }
      Ok(text) ->
        match round_trips(text) {
          Ok(_) -> Tally { pass: 1, fail: 0, impl_ok: 0, impl_no: 0 }
          Err(e) -> {
            println("FAIL " ++ name ++ ": accepted but " ++ e)
            Tally { pass: 0, fail: 1, impl_ok: 0, impl_no: 0 }
          }
        }
    }
  } else if str.starts_with(name, "n_") {
    match v {
      Ok(_) -> {
        println("FAIL " ++ name ++ ": must reject, was accepted")
        Tally { pass: 0, fail: 1, impl_ok: 0, impl_no: 0 }
      }
      Err(_) -> Tally { pass: 1, fail: 0, impl_ok: 0, impl_no: 0 }
    }
  } else {
    match v {
      Ok(_) -> Tally { pass: 0, fail: 0, impl_ok: 1, impl_no: 0 }
      Err(_) -> Tally { pass: 0, fail: 0, impl_ok: 0, impl_no: 1 }
    }
  }
}

# `run` reports rather than returns: a Dawn program has no exit-code builtin, so
# the gate is the panic at the end of `main` when `fail > 0`.
pub fn run(dir: String) -> Tally !io =
  match io.list_dir(dir) {
    Err(e) -> {
      println("FAIL: cannot list " ++ dir ++ ": " ++ e.message)
      Tally { pass: 0, fail: 1, impl_ok: 0, impl_no: 0 }
    }
    Ok(names) -> {
      var t = Tally { pass: 0, fail: 0, impl_ok: 0, impl_no: 0 }
      let sorted = list.sort(names)
      var i = 0
      while i < len(sorted) {
        let name = unwrap_or(get(sorted, i), "")
        if str.ends_with(name, ".json") {
          t = add(t, check_one(dir, name))
        }
        i = i + 1
      }
      println("JSONTestSuite: " ++ to_string(t.pass) ++ " mandatory passed, " ++
          to_string(t.fail) ++ " failed; " ++ to_string(t.impl_ok) ++ " i_ accepted, " ++
          to_string(t.impl_no) ++ " i_ rejected")
      t
    }
  }

test "the y_/n_ verdict rules are what they say" {
  # a fixture name decides which of the three rules applies; the rules
  # themselves are exercised here so a typo in a prefix test is not silent
  assert str.starts_with("y_array_null.json", "y_")
  assert str.starts_with("n_object_trailing_comma.json", "n_")
  assert not str.starts_with("i_number_huge_exp.json", "y_")
  assert not str.starts_with("i_number_huge_exp.json", "n_")
}

fn trips(text: String) -> Bool =
  match round_trips(text) {
    Ok(_) -> true
    Err(_) -> false
  }

test "round_trips catches a renderer that breaks its own parser" {
  assert trips("{\"a\":[1,2,3],\"b\":\"x\"}")
  # a string holding every C0 control: this is the case that used to render as
  # raw bytes and fail to re-parse
  assert trips("\"\\u0000\\u0001\\b\\f\\n\\r\\t\\u001f\"")
  assert trips("\"\\ud834\\udd1e\"")
}