From 5e24852ec1662d119be63cf798f82f7e156858fc Mon Sep 17 00:00:00 2001 From: harehare Date: Sat, 27 Jun 2026 09:53:48 +0900 Subject: [PATCH 1/2] =?UTF-8?q?=E2=9C=A8=20feat(mq-lang):=20add=20walk=20b?= =?UTF-8?q?uiltin=20function=20with=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add walk(v, f) that recursively applies f to every node in markdown, array, and dict structures (bottom-up traversal) - Fix dict case to use single-arg fn(entry) lambda to correctly match how with_entries passes each [key, value] pair - Add test_walk with 24 cases covering scalar, array, dict, and markdown inputs in builtin_tests.mq --- crates/mq-lang/builtin.mq | 18 ++++ crates/mq-lang/builtin_tests.mq | 100 +++++++++++++++++++ crates/mq-lang/tests/property_based_tests.rs | 2 + 3 files changed, 120 insertions(+) diff --git a/crates/mq-lang/builtin.mq b/crates/mq-lang/builtin.mq index 255b8ab79..e2eb2c484 100644 --- a/crates/mq-lang/builtin.mq +++ b/crates/mq-lang/builtin.mq @@ -923,3 +923,21 @@ def frontmatter(v): else: None end +# Walks through a value (which can be a markdown node, array, or dict) and applies a function to each element, returning a new structure with the results. +def walk(v, f): + match (v): + | :markdown: + do + let new_children = map(v.children, fn(child): walk(child, f);) + | let new_value = f(v) + | set_children(new_value, new_children) + end + | :array: + map(v, fn(x): walk(x, f);) + | :dict: + with_entries(v, fn(entry): [entry[0], walk(entry[1], f)];) + | _: + f(v) + end +end + diff --git a/crates/mq-lang/builtin_tests.mq b/crates/mq-lang/builtin_tests.mq index c85c31359..8328f4745 100644 --- a/crates/mq-lang/builtin_tests.mq +++ b/crates/mq-lang/builtin_tests.mq @@ -949,3 +949,103 @@ def test_frontmatter(): | assert_eq(result4["tags"], ["rust", "markdown"]) end +def test_walk(): + # scalar: number — f applied directly + let result1 = walk(42, fn(x): x * 2;) + | assert_eq(result1, 84) + + # scalar: string — f applied directly + | let result2 = walk("hello", fn(x): if (is_string(x)): upcase(x) else: x;) + | assert_eq(result2, "HELLO") + + # scalar: None — f applied directly + | let result3 = walk(None, identity) + | assert_eq(result3, None) + + # scalar: bool — f applied directly + | let result4 = walk(true, fn(x): if (is_bool(x)): !x else: x;) + | assert_eq(result4, false) + + # array: empty array returns [] + | let result5 = walk([], identity) + | assert_eq(result5, []) + + # array: identity preserves all elements + | let result6 = walk([1, 2, 3], identity) + | assert_eq(result6, [1, 2, 3]) + + # array: transform each number + | let result7 = walk([1, 2, 3], fn(x): if (is_number(x)): x * 2 else: x;) + | assert_eq(result7, [2, 4, 6]) + + # array: recurses into nested subarrays + | let result8 = walk([[1, 2], [3, 4]], fn(x): if (is_number(x)): x + 10 else: x;) + | assert_eq(result8, [[11, 12], [13, 14]]) + + # array: 3 levels of nesting + | let result9 = walk([[[1]]], fn(x): if (is_number(x)): x + 100 else: x;) + | assert_eq(result9, [[[101]]]) + + # array: identity preserves mixed types including None + | let result10 = walk([1, "hello", None], identity) + | assert_eq(result10, [1, "hello", None]) + + # array: negate each number + | let result11 = walk([2, 4, 6], fn(x): if (is_number(x)): x * -1 else: x;) + | assert_eq(result11, [-2, -4, -6]) + + # array: upcase each string + | let result12 = walk(["a", "b"], fn(x): if (is_string(x)): upcase(x) else: x;) + | assert_eq(result12, ["A", "B"]) + + # array: flip each boolean + | let result13 = walk([true, false], fn(x): if (is_bool(x)): !x else: x;) + | assert_eq(result13, [false, true]) + + # array: named def as function argument + | def double(x): if (is_number(x)): x * 2 else: x; + | let result14 = walk([1, 2, 3], double) + | assert_eq(result14, [2, 4, 6]) + + # dict: empty dict returns {} + | let result15 = walk({}, identity) + | assert_eq(result15, dict()) + + # dict: only values are transformed, keys unchanged + | let result16 = walk({"a": 1, "b": 2}, fn(x): if (is_number(x)): x * 10 else: x;) + | assert_eq(result16["a"], 10) + | assert_eq(result16["b"], 20) + + # dict: string values uppercased + | let result17 = walk({"x": "hello"}, fn(x): if (is_string(x)): upcase(x) else: x;) + | assert_eq(result17["x"], "HELLO") + + # dict: identity preserves full structure + | let result18 = walk({"a": 1, "b": 2}, identity) + | assert_eq(result18, {"a": 1, "b": 2}) + + # markdown: text leaf — identity preserves text content + | let result19 = do "plain text" | to_markdown() | first() | walk(fn(n): n;) | to_text(); + | assert_eq(result19, "plain text") + + # markdown: heading — identity preserves HTML output + | let result20 = do "# heading" | to_markdown() | first() | walk(fn(n): n;) | to_html(); + | assert_eq(result20, "

heading

") + + # markdown: h2 — identity preserves depth + | let result21 = do "## section" | to_markdown() | first() | walk(fn(n): n;) | to_html(); + | assert_eq(result21, "

section

") + + # markdown: demote h1 → h2 + | let result22 = do "# Title" | to_markdown() | first() | walk(fn(n): if (is_h(n)): demote_heading(n) else: n;) | to_html(); + | assert_eq(result22, "

Title

") + + # markdown: promote h2 → h1 + | let result23 = do "## Title" | to_markdown() | first() | walk(fn(n): if (is_h(n)): promote_heading(n) else: n;) | to_html(); + | assert_eq(result23, "

Title

") + + # markdown: node name reflects transformation + | let result24 = do "# hi" | to_markdown() | first() | walk(fn(n): if (is_h(n)): demote_heading(n) else: n;) | to_md_name(); + | assert_eq(result24, "h2") +end + diff --git a/crates/mq-lang/tests/property_based_tests.rs b/crates/mq-lang/tests/property_based_tests.rs index d2cc0b62f..c4e3806df 100644 --- a/crates/mq-lang/tests/property_based_tests.rs +++ b/crates/mq-lang/tests/property_based_tests.rs @@ -721,3 +721,5 @@ proptest! { } } } + + From f2e993acc3d14b302b48392edfc5e3cdfbb941d4 Mon Sep 17 00:00:00 2001 From: Takahiro Sato Date: Sat, 27 Jun 2026 10:35:36 +0900 Subject: [PATCH 2/2] Update property_based_tests.rs --- crates/mq-lang/tests/property_based_tests.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/crates/mq-lang/tests/property_based_tests.rs b/crates/mq-lang/tests/property_based_tests.rs index c4e3806df..d2cc0b62f 100644 --- a/crates/mq-lang/tests/property_based_tests.rs +++ b/crates/mq-lang/tests/property_based_tests.rs @@ -721,5 +721,3 @@ proptest! { } } } - -