Created
January 24, 2026 13:28
-
-
Save mizchi/978a21f5349e217b9cb72e2bb956451b to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // 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"); | |
| } |
Author
mizchi
commented
Jan 24, 2026
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment