Skip to content

Instantly share code, notes, and snippets.

@mizchi
Created January 24, 2026 13:28
Show Gist options
  • Select an option

  • Save mizchi/978a21f5349e217b9cb72e2bb956451b to your computer and use it in GitHub Desktop.

Select an option

Save mizchi/978a21f5349e217b9cb72e2bb956451b to your computer and use it in GitHub Desktop.
// JSON Parser in Wado - TDD approach
// Goal: Verify if Wado can implement a JSON parser
use {println, Stdout} from "core:cli";
// =============================================================================
// Parser State
// =============================================================================
struct Parser {
input: String,
pos: i32,
}
impl Parser {
fn new(input: String) -> Parser {
return Parser { input, pos: 0 };
}
fn peek(&self) -> i32 {
if self.pos >= self.input.len() {
return -1; // EOF
}
return self.input.get(self.pos);
}
fn advance(&mut self) {
self.pos += 1;
}
fn skip_whitespace(&mut self) {
loop {
let c = self.peek();
// space, tab, newline, carriage return
if c == 32 || c == 9 || c == 10 || c == 13 {
self.advance();
} else {
break;
}
}
}
fn is_eof(&self) -> bool {
return self.pos >= self.input.len();
}
}
// =============================================================================
// Tests: Parser basics
// =============================================================================
test "parser-peek-and-advance" {
let mut p = Parser::new("abc");
assert p.peek() == 97; // 'a'
p.advance();
assert p.peek() == 98; // 'b'
p.advance();
assert p.peek() == 99; // 'c'
p.advance();
assert p.peek() == -1; // EOF
}
test "parser-skip-whitespace" {
let mut p = Parser::new(" \t\nabc");
p.skip_whitespace();
assert p.peek() == 97; // 'a'
}
test "parser-is-eof" {
let mut p = Parser::new("a");
assert !p.is_eof();
p.advance();
assert p.is_eof();
}
// =============================================================================
// Parse null
// =============================================================================
impl Parser {
// Returns true if successfully parsed "null"
fn parse_null(&mut self) -> bool {
self.skip_whitespace();
if self.match_literal("null") {
return true;
}
return false;
}
// Helper: match a literal string
fn match_literal(&mut self, lit: String) -> bool {
let start = self.pos;
for let mut i = 0; i < lit.len(); i += 1 {
if self.peek() != lit.get(i) {
self.pos = start; // backtrack
return false;
}
self.advance();
}
return true;
}
}
test "parse-null-valid" {
let mut p = Parser::new("null");
assert p.parse_null();
assert p.is_eof();
}
test "parse-null-with-whitespace" {
let mut p = Parser::new(" null");
assert p.parse_null();
}
test "parse-null-invalid" {
let mut p = Parser::new("nul");
assert !p.parse_null();
}
// =============================================================================
// Parse boolean
// =============================================================================
// Since we can't return Option<bool> with pattern matching easily,
// use a result struct
struct BoolResult {
ok: bool,
value: bool,
}
impl Parser {
fn parse_bool(&mut self) -> BoolResult {
self.skip_whitespace();
if self.match_literal("true") {
return BoolResult { ok: true, value: true };
}
if self.match_literal("false") {
return BoolResult { ok: true, value: false };
}
return BoolResult { ok: false, value: false };
}
}
test "parse-bool-true" {
let mut p = Parser::new("true");
let r = p.parse_bool();
assert r.ok;
assert r.value;
}
test "parse-bool-false" {
let mut p = Parser::new("false");
let r = p.parse_bool();
assert r.ok;
assert !r.value;
}
test "parse-bool-invalid" {
let mut p = Parser::new("tru");
let r = p.parse_bool();
assert !r.ok;
}
// =============================================================================
// Parse number (integer only for now)
// =============================================================================
struct NumberResult {
ok: bool,
value: i32,
}
impl Parser {
fn parse_number(&mut self) -> NumberResult {
self.skip_whitespace();
let start = self.pos;
// Handle negative
let mut negative = false;
if self.peek() == 45 { // '-'
negative = true;
self.advance();
}
// Must have at least one digit
if !self.is_digit(self.peek()) {
self.pos = start;
return NumberResult { ok: false, value: 0 };
}
let mut value = 0;
loop {
let c = self.peek();
if self.is_digit(c) {
value = value * 10 + (c - 48);
self.advance();
} else {
break;
}
}
if negative {
value = -value;
}
return NumberResult { ok: true, value };
}
fn is_digit(&self, c: i32) -> bool {
return c >= 48 && c <= 57; // '0' to '9'
}
}
test "parse-number-positive" {
let mut p = Parser::new("42");
let r = p.parse_number();
assert r.ok;
assert r.value == 42;
}
test "parse-number-negative" {
let mut p = Parser::new("-123");
let r = p.parse_number();
assert r.ok;
assert r.value == -123;
}
test "parse-number-zero" {
let mut p = Parser::new("0");
let r = p.parse_number();
assert r.ok;
assert r.value == 0;
}
test "parse-number-invalid" {
let mut p = Parser::new("abc");
let r = p.parse_number();
assert !r.ok;
}
// =============================================================================
// Parse string (simplified - just detect quotes, no content extraction)
// =============================================================================
struct StringResult {
ok: bool,
start: i32,
end: i32,
}
impl Parser {
// Parse a string and return the start/end positions (excluding quotes)
fn parse_string(&mut self) -> StringResult {
self.skip_whitespace();
if self.peek() != 34 { // '"'
return StringResult { ok: false, start: 0, end: 0 };
}
self.advance(); // consume opening quote
let start = self.pos;
loop {
let c = self.peek();
if c == -1 {
// Unexpected EOF
return StringResult { ok: false, start: 0, end: 0 };
}
if c == 34 { // closing '"'
let end = self.pos;
self.advance();
return StringResult { ok: true, start, end };
}
if c == 92 { // backslash '\' - skip escape sequence
self.advance();
if self.peek() != -1 {
self.advance();
}
} else {
self.advance();
}
}
// unreachable
return StringResult { ok: false, start: 0, end: 0 };
}
}
test "parse-string-simple" {
let mut p = Parser::new("\"hello\"");
let r = p.parse_string();
assert r.ok;
assert r.start == 1;
assert r.end == 6;
}
test "parse-string-empty" {
let mut p = Parser::new("\"\"");
let r = p.parse_string();
assert r.ok;
assert r.start == 1;
assert r.end == 1;
}
test "parse-string-unclosed" {
let mut p = Parser::new("\"hello");
let r = p.parse_string();
assert !r.ok;
}
test "parse-string-with-escape" {
let mut p = Parser::new("\"hello\\nworld\"");
let r = p.parse_string();
assert r.ok;
// "hello\nworld" - content is from pos 1 to 13
}
// =============================================================================
// Parse array (counts elements, does not store them)
// =============================================================================
struct ArrayResult {
ok: bool,
count: i32,
}
impl Parser {
// Parse a JSON array and count its elements
fn parse_array(&mut self) -> ArrayResult {
self.skip_whitespace();
if self.peek() != 91 { // '['
return ArrayResult { ok: false, count: 0 };
}
self.advance(); // consume '['
self.skip_whitespace();
// Empty array
if self.peek() == 93 { // ']'
self.advance();
return ArrayResult { ok: true, count: 0 };
}
let mut count = 0;
loop {
// Parse a value (any JSON value)
if !self.skip_value() {
return ArrayResult { ok: false, count: 0 };
}
count += 1;
self.skip_whitespace();
let c = self.peek();
if c == 93 { // ']'
self.advance();
return ArrayResult { ok: true, count };
}
if c == 44 { // ','
self.advance();
self.skip_whitespace();
} else {
// Expected ',' or ']'
return ArrayResult { ok: false, count: 0 };
}
}
// unreachable
return ArrayResult { ok: false, count: 0 };
}
// Skip any JSON value (for array/object parsing)
fn skip_value(&mut self) -> bool {
self.skip_whitespace();
let c = self.peek();
// null
if c == 110 { // 'n'
return self.match_literal("null");
}
// true
if c == 116 { // 't'
return self.match_literal("true");
}
// false
if c == 102 { // 'f'
return self.match_literal("false");
}
// string
if c == 34 { // '"'
let r = self.parse_string();
return r.ok;
}
// array
if c == 91 { // '['
let r = self.parse_array();
return r.ok;
}
// object
if c == 123 { // '{'
let r = self.parse_object();
return r.ok;
}
// number (starts with digit or '-')
if self.is_digit(c) || c == 45 {
let r = self.parse_number();
return r.ok;
}
return false;
}
}
test "parse-array-empty" {
let mut p = Parser::new("[]");
let r = p.parse_array();
assert r.ok;
assert r.count == 0;
}
test "parse-array-single-number" {
let mut p = Parser::new("[42]");
let r = p.parse_array();
assert r.ok;
assert r.count == 1;
}
test "parse-array-multiple-numbers" {
let mut p = Parser::new("[1, 2, 3]");
let r = p.parse_array();
assert r.ok;
assert r.count == 3;
}
test "parse-array-mixed-types" {
let mut p = Parser::new("[1, \"hello\", true, null]");
let r = p.parse_array();
assert r.ok;
assert r.count == 4;
}
test "parse-array-nested" {
let mut p = Parser::new("[[1, 2], [3, 4], [5]]");
let r = p.parse_array();
assert r.ok;
assert r.count == 3;
}
test "parse-array-unclosed" {
let mut p = Parser::new("[1, 2");
let r = p.parse_array();
assert !r.ok;
}
// =============================================================================
// Parse object (counts key-value pairs, does not store them)
// =============================================================================
struct ObjectResult {
ok: bool,
count: i32,
}
impl Parser {
// Parse a JSON object and count its key-value pairs
fn parse_object(&mut self) -> ObjectResult {
self.skip_whitespace();
if self.peek() != 123 { // '{'
return ObjectResult { ok: false, count: 0 };
}
self.advance(); // consume '{'
self.skip_whitespace();
// Empty object
if self.peek() == 125 { // '}'
self.advance();
return ObjectResult { ok: true, count: 0 };
}
let mut count = 0;
loop {
// Parse key (must be string)
self.skip_whitespace();
let key_result = self.parse_string();
if !key_result.ok {
return ObjectResult { ok: false, count: 0 };
}
// Expect ':'
self.skip_whitespace();
if self.peek() != 58 { // ':'
return ObjectResult { ok: false, count: 0 };
}
self.advance();
// Parse value
if !self.skip_value() {
return ObjectResult { ok: false, count: 0 };
}
count += 1;
self.skip_whitespace();
let c = self.peek();
if c == 125 { // '}'
self.advance();
return ObjectResult { ok: true, count };
}
if c == 44 { // ','
self.advance();
} else {
// Expected ',' or '}'
return ObjectResult { ok: false, count: 0 };
}
}
// unreachable
return ObjectResult { ok: false, count: 0 };
}
}
test "parse-object-empty" {
let mut p = Parser::new("{}");
let r = p.parse_object();
assert r.ok;
assert r.count == 0;
}
test "parse-object-single-pair" {
let mut p = Parser::new("{\"name\": \"John\"}");
let r = p.parse_object();
assert r.ok;
assert r.count == 1;
}
test "parse-object-multiple-pairs" {
let mut p = Parser::new("{\"a\": 1, \"b\": 2, \"c\": 3}");
let r = p.parse_object();
assert r.ok;
assert r.count == 3;
}
test "parse-object-nested" {
let mut p = Parser::new("{\"outer\": {\"inner\": 42}}");
let r = p.parse_object();
assert r.ok;
assert r.count == 1;
}
test "parse-object-with-array" {
let mut p = Parser::new("{\"items\": [1, 2, 3]}");
let r = p.parse_object();
assert r.ok;
assert r.count == 1;
}
test "parse-object-unclosed" {
let mut p = Parser::new("{\"a\": 1");
let r = p.parse_object();
assert !r.ok;
}
// =============================================================================
// JsonValue - using type tag pattern (workaround for variant codegen)
// =============================================================================
// Type tags for JSON values
// Note: Using constants would be cleaner but Wado doesn't have const yet
// NULL = 0, BOOL = 1, NUMBER = 2, STRING = 3, ARRAY = 4, OBJECT = 5
struct JsonValue {
tag: i32,
// For BOOL: 0 = false, 1 = true
// For NUMBER: the numeric value
// For STRING/ARRAY/OBJECT: start position in source
int_value: i32,
// For STRING: end position in source
// For ARRAY/OBJECT: count of elements
int_value2: i32,
}
impl JsonValue {
fn make_null() -> JsonValue {
return JsonValue { tag: 0, int_value: 0, int_value2: 0 };
}
fn make_bool(b: bool) -> JsonValue {
let v = if b { 1 } else { 0 };
return JsonValue { tag: 1, int_value: v, int_value2: 0 };
}
fn make_number(n: i32) -> JsonValue {
return JsonValue { tag: 2, int_value: n, int_value2: 0 };
}
fn make_string(start: i32, end: i32) -> JsonValue {
return JsonValue { tag: 3, int_value: start, int_value2: end };
}
fn make_array(count: i32) -> JsonValue {
return JsonValue { tag: 4, int_value: 0, int_value2: count };
}
fn make_object(count: i32) -> JsonValue {
return JsonValue { tag: 5, int_value: 0, int_value2: count };
}
fn is_null(&self) -> bool {
return self.tag == 0;
}
fn is_bool(&self) -> bool {
return self.tag == 1;
}
fn is_number(&self) -> bool {
return self.tag == 2;
}
fn is_string(&self) -> bool {
return self.tag == 3;
}
fn is_array(&self) -> bool {
return self.tag == 4;
}
fn is_object(&self) -> bool {
return self.tag == 5;
}
fn as_bool(&self) -> bool {
return self.int_value != 0;
}
fn as_number(&self) -> i32 {
return self.int_value;
}
fn array_count(&self) -> i32 {
return self.int_value2;
}
fn object_count(&self) -> i32 {
return self.int_value2;
}
}
struct ParseResult {
ok: bool,
value: JsonValue,
}
impl Parser {
// Parse any JSON value and return a JsonValue
fn parse_value(&mut self) -> ParseResult {
self.skip_whitespace();
let c = self.peek();
// null
if c == 110 { // 'n'
if self.match_literal("null") {
return ParseResult { ok: true, value: JsonValue::make_null() };
}
return ParseResult { ok: false, value: JsonValue::make_null() };
}
// true
if c == 116 { // 't'
if self.match_literal("true") {
return ParseResult { ok: true, value: JsonValue::make_bool(true) };
}
return ParseResult { ok: false, value: JsonValue::make_null() };
}
// false
if c == 102 { // 'f'
if self.match_literal("false") {
return ParseResult { ok: true, value: JsonValue::make_bool(false) };
}
return ParseResult { ok: false, value: JsonValue::make_null() };
}
// string
if c == 34 { // '"'
let r = self.parse_string();
if r.ok {
return ParseResult { ok: true, value: JsonValue::make_string(r.start, r.end) };
}
return ParseResult { ok: false, value: JsonValue::make_null() };
}
// array
if c == 91 { // '['
let r = self.parse_array();
if r.ok {
return ParseResult { ok: true, value: JsonValue::make_array(r.count) };
}
return ParseResult { ok: false, value: JsonValue::make_null() };
}
// object
if c == 123 { // '{'
let r = self.parse_object();
if r.ok {
return ParseResult { ok: true, value: JsonValue::make_object(r.count) };
}
return ParseResult { ok: false, value: JsonValue::make_null() };
}
// number
if self.is_digit(c) || c == 45 { // digit or '-'
let r = self.parse_number();
if r.ok {
return ParseResult { ok: true, value: JsonValue::make_number(r.value) };
}
return ParseResult { ok: false, value: JsonValue::make_null() };
}
return ParseResult { ok: false, value: JsonValue::make_null() };
}
}
test "json-value-null" {
let mut p = Parser::new("null");
let r = p.parse_value();
assert r.ok;
assert r.value.is_null();
}
test "json-value-bool-true" {
let mut p = Parser::new("true");
let r = p.parse_value();
assert r.ok;
assert r.value.is_bool();
assert r.value.as_bool();
}
test "json-value-bool-false" {
let mut p = Parser::new("false");
let r = p.parse_value();
assert r.ok;
assert r.value.is_bool();
assert !r.value.as_bool();
}
test "json-value-number" {
let mut p = Parser::new("42");
let r = p.parse_value();
assert r.ok;
assert r.value.is_number();
assert r.value.as_number() == 42;
}
test "json-value-string" {
let mut p = Parser::new("\"hello\"");
let r = p.parse_value();
assert r.ok;
assert r.value.is_string();
}
test "json-value-array" {
let mut p = Parser::new("[1, 2, 3]");
let r = p.parse_value();
assert r.ok;
assert r.value.is_array();
assert r.value.array_count() == 3;
}
test "json-value-object" {
let mut p = Parser::new("{\"a\": 1, \"b\": 2}");
let r = p.parse_value();
assert r.ok;
assert r.value.is_object();
assert r.value.object_count() == 2;
}
test "json-complex-document" {
// A more realistic JSON document
let mut p = Parser::new("{\"name\": \"test\", \"values\": [1, 2, 3], \"active\": true}");
let r = p.parse_value();
assert r.ok;
assert r.value.is_object();
assert r.value.object_count() == 3;
}
// =============================================================================
// Entry point for manual testing
// =============================================================================
fn run() with Stdout {
println("JSON Parser Tests");
println("Run with: wado test example/json.wado");
}
@mizchi
Copy link
Author

mizchi commented Jan 24, 2026

  検証で判明した、JSON パーサーを完成させるために必要な機能:          
                                                                       
  1. Array<i32> の codegen 修正 - 任意の配列型のサポート               
  2. String::from_bytes() の追加 - バイト列から文字列を構築            
  3. variant codegen - sum type のネイティブサポート(大きい)         
  4. 浮動小数点パース - f64 対応                                       
                                                                       
  どの機能から実装しますか?優先度の観点:                             
  ┌─────────────────────────┬──────────────────────┬────────┐          
  │          機能           │       影響範囲       │ 難易度 │          
  ├─────────────────────────┼──────────────────────┼────────┤          
  │ Array<i32> codegen 修正 │ JSON パーサー + 汎用 │ 中     │          
  ├─────────────────────────┼──────────────────────┼────────┤          
  │ String::from_bytes()    │ JSON パーサー限定    │ 小     │          
  ├─────────────────────────┼──────────────────────┼────────┤          
  │ variant codegen         │ 言語全体             │ 大     │          
  ├─────────────────────────┼──────────────────────┼────────┤          
  │ f64 パース              │ JSON パーサー限定    │ 小     │          
  └─────────────────────────┴──────────────────────┴────────┘   

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment