From 9d3d8ef2be15dfbf279de71241ff747a568e2c49 Mon Sep 17 00:00:00 2001 From: Li Jin Date: Fri, 18 Jul 2025 11:51:39 +0800 Subject: Added specs, tests and docs. --- doc/docs/doc/README.md | 107 +++++++++++++++++++++++++++++++++++++++++ doc/docs/zh/doc/README.md | 107 +++++++++++++++++++++++++++++++++++++++++ spec/inputs/macro.yue | 54 +++++++++++++++------ spec/inputs/macro_export.yue | 31 ++++++------ spec/inputs/macro_teal.yue | 13 +++-- spec/inputs/macro_todo.yue | 7 +-- spec/inputs/string.yue | 73 ++++++++++++++++++++++++++++ spec/outputs/macro.lua | 13 ++++- spec/outputs/string.lua | 41 +++++++++++++++- src/yuescript/yue_compiler.cpp | 46 ++++++++++-------- src/yuescript/yue_parser.cpp | 4 +- src/yuescript/yue_parser.h | 2 +- 12 files changed, 435 insertions(+), 63 deletions(-) diff --git a/doc/docs/doc/README.md b/doc/docs/doc/README.md index 7135a26..f0d67a5 100755 --- a/doc/docs/doc/README.md +++ b/doc/docs/doc/README.md @@ -444,6 +444,54 @@ print "Valid enum type:", $BodyType Static +### Argument Validation + +You can declare the expected AST node types in the argument list, and check whether the incoming macro arguments meet the expectations at compile time. + +```moonscript +macro printNumAndStr = (num `Num, str `String) -> | + print( + #{num} + #{str} + ) + +$printNumAndStr 123, "hello" +``` + +
+macro printNumAndStr = (num `Num, str `String) -> |
+  print(
+    #{num}
+    #{str}
+  )
+
+$printNumAndStr 123, "hello"
+
+
+ +If you need more flexible argument checking, you can use the built-in `$is_ast` macro function to manually check at the appropriate place. + +```moonscript +macro printNumAndStr = (num, str) -> + error "expected Num as first argument" unless $is_ast Num, num + error "expected String as second argument" unless $is_ast String, str + "print(#{num}, #{str})" + +$printNumAndStr 123, "hello" +``` + +
+macro printNumAndStr = (num, str) ->
+  error "expected Num as first argument" unless $is_ast Num, num
+  error "expected String as second argument" unless $is_ast String, str
+  "print(#{num}, #{str})"
+
+$printNumAndStr 123, "hello"
+
+
+ +For more details about available AST nodes, please refer to the uppercased definitions in [yue_parser.cpp](https://github.com/IppClub/YueScript/blob/main/src/yuescript/yue_parser.cpp). + ## Operator All of Lua's binary and unary operators are available. Additionally **!=** is as an alias for **~=**, and either **\\** or **::** can be used to write a chaining function call like `tb\func!` or `tb::func!`. And Yuescipt offers some other special operators to write more expressive codes. @@ -1702,6 +1750,65 @@ binary = 0B10011 +### YAML Multiline String + +The `|` prefix introduces a YAML-style multiline string literal: + +```moonscript +str = | + key: value + list: + - item1 + - #{expr} +``` + +
+str = |
+  key: value
+  list:
+    - item1
+    - #{expr}
+
+
+ +This allows writing structured multiline text conveniently. All line breaks and indentation are preserved relative to the first non-empty line, and expressions inside `#{...}` are interpolated automatically as `tostring(expr)`. + +YAML Multiline String automatically detects the common leading whitespace prefix (minimum indentation across all non-empty lines) and removes it from all lines. This makes it easy to indent your code visually without affecting the resulting string content. + +```moonscript +fn = -> + str = | + foo: + bar: baz + return str +``` + +
+fn = ->
+  str = |
+    foo:
+      bar: baz
+  return str
+
+
+ +Internal indentation is preserved relative to the removed common prefix, allowing clean nested structures. + +All special characters like quotes (`"`) and backslashes (`\`) in the YAMLMultiline block are automatically escaped so that the generated Lua string is syntactically valid and behaves as expected. + +```moonscript +str = | + path: "C:\Program Files\App" + note: 'He said: "#{Hello}!"' +``` + +
+str = |
+  path: "C:\Program Files\App"
+  note: 'He said: "#{Hello}!"'
+
+
+ ## Function Literals All functions are created using a function expression. A simple function is denoted using the arrow: **->**. diff --git a/doc/docs/zh/doc/README.md b/doc/docs/zh/doc/README.md index 61b746c..15f4768 100755 --- a/doc/docs/zh/doc/README.md +++ b/doc/docs/zh/doc/README.md @@ -442,6 +442,54 @@ print "有效的枚举类型:", $BodyType Static +### 宏参数检查 + +可以直接在参数列表中声明期望的 AST 节点类型,并在编译时检查传入的宏参数是否符合预期。 + +```moonscript +macro printNumAndStr = (num `Num, str `String) -> | + print( + #{num} + #{str} + ) + +$printNumAndStr 123, "hello" +``` + +
+macro printNumAndStr = (num `Num, str `String) -> |
+  print(
+    #{num}
+    #{str}
+  )
+
+$printNumAndStr 123, "hello"
+
+
+ +如果需要做更加灵活的参数检查操作,可以使用内置的 `$is_ast` 宏函数在合适的位置进行手动检查。 + +```moonscript +macro printNumAndStr = (num, str) -> + error "expected Num as first argument" unless $is_ast Num, num + error "expected String as second argument" unless $is_ast String, str + "print(#{num}, #{str})" + +$printNumAndStr 123, "hello" +``` + +
+macro printNumAndStr = (num, str) ->
+  error "expected Num as first argument" unless $is_ast Num, num
+  error "expected String as second argument" unless $is_ast String, str
+  "print(#{num}, #{str})"
+
+$printNumAndStr 123, "hello"
+
+
+ +更多关于可用 AST 节点的详细信息,请参考 [yue_parser.cpp](https://github.com/IppClub/YueScript/blob/main/src/yuescript/yue_parser.cpp) 中大写的规则定义。 + ## 操作符 Lua的所有二元和一元操作符在月之脚本中都是可用的。此外,**!=** 符号是 **~=** 的别名,而 **\\** 或 **::** 均可用于编写链式函数调用,如写作 `tb\func!` 或 `tb::func!`。此外月之脚本还提供了一些其他特殊的操作符,以编写更具表达力的代码。 @@ -1700,6 +1748,65 @@ binary = 0B10011 +### YAML 风格字符串 + +使用 `|` 前缀标记一个多行 YAML 风格字符串: + +```moonscript +str = | + key: value + list: + - item1 + - #{expr} +``` + +
+str = |
+  key: value
+  list:
+    - item1
+    - #{expr}
+
+
+ +其效果类似于原生 Lua 的多行拼接,所有文本(含换行)将被保留下来,并支持 `#{...}` 语法,通过 `tostring(expr)` 插入表达式结果。 + +YAML 风格的多行字符串会自动检测首行后最小的公共缩进,并从所有行中删除该前缀空白字符。这让你可以在代码中对齐文本,但输出字符串不会带多余缩进。 + +```moonscript +fn = -> + str = | + foo: + bar: baz + return str +``` + +
+fn = ->
+  str = |
+    foo:
+      bar: baz
+  return str
+
+
+ +输出字符串中的 foo: 对齐到行首,不会带有函数缩进空格。保留内部缩进的相对结构,适合书写结构化嵌套样式的内容。 + +支持自动处理字符中的引号、反斜杠等特殊符号,无需手动转义: + +```moonscript +str = | + path: "C:\Program Files\App" + note: 'He said: "#{Hello}!"' +``` + +
+str = |
+  path: "C:\Program Files\App"
+  note: 'He said: "#{Hello}!"'
+
+
+ ## 函数字面量 所有函数都是使用月之脚本的函数表达式创建的。一个简单的函数可以用箭头表示为:**->**。 diff --git a/spec/inputs/macro.yue b/spec/inputs/macro.yue index 5d5f1a9..191f09f 100644 --- a/spec/inputs/macro.yue +++ b/spec/inputs/macro.yue @@ -60,6 +60,11 @@ macro NumAndStr = (num, str) -> print $NumAndStr 123, 'xyz' +macro NumAndStr2 = (num`Num, str`SingleString) -> | + [#{num}, #{str}] + +print $NumAndStr2 456, 'abc' + $asserts item == nil $myconfig false @@ -100,13 +105,14 @@ macro filter = (items, action)-> $showMacro "filter", "[_ for _ in *#{items} when #{action}]" macro reduce = (items, def, action)-> - $showMacro "reduce", "if ##{items} == 0 - #{def} -else - _1 = #{def} - for _2 in *#{items} - _1 = #{action} - _1" + $showMacro "reduce", | + if ##{items} == 0 + #{def} + else + _1 = #{def} + for _2 in *#{items} + _1 = #{action} + _1 macro foreach = (items, action)-> $showMacro "foreach", "for _ in *#{items} @@ -154,13 +160,15 @@ macro curry = (...)-> f = $curry x,y,z,do print x,y,z -macro get_inner = (var)-> "do - a = 1 - a + 1" +macro get_inner = (var)-> | + do + a = 1 + a + 1 -macro get_inner_hygienic = (var)-> "(-> - local a = 1 - a + 1)!" +macro get_inner_hygienic = (var)-> | + (-> + local a = 1 + a + 1)! do a = 8 @@ -196,6 +204,18 @@ end print x +import "yue" +macro lua = (code`YAMLMultiline) -> { + code: yue.loadstring(code)! + type: "lua" +} + +$lua | + local function f2(a) + return a + 1 + end + x = x + f2(3) + macro def = (fname, ...)-> args = {...} last = table.remove args @@ -317,7 +337,13 @@ $chainC( Destroy! ) -macro tb = -> "{'abc', a:123, :=> 998}" +macro tb = -> | + { + 'abc' + a: 123 + : => 998 + } + print $tb[1], $tb.a, ($tb)!, $tb! print "current line: #{ $LINE }" diff --git a/spec/inputs/macro_export.yue b/spec/inputs/macro_export.yue index 75fd813..22905b5 100644 --- a/spec/inputs/macro_export.yue +++ b/spec/inputs/macro_export.yue @@ -8,13 +8,12 @@ export macro config = (debugging = true)-> "" export macro showMacro = (name, res)-> - if debugMacro then " -do - txt = #{res} - print '[macro ' .. #{name} .. ']' - print txt - txt -" + if debugMacro then | + do + txt = #{res} + print '[macro #{name}]' + print txt + txt else res @@ -35,14 +34,16 @@ export macro copy = (src, dst, ...)-> src != "_src_" and src != "_dst_" and dst != "_src_" and dst != "_dst_" "copy targets can not be _src_ or _dst_" ) - " -do - local _src_, _dst_ - with _dst_ := #{dst} - with _src_ := #{src} -#{table.concat for field in *{...} do " - _dst_.#{field} = _src_.#{field} -"}" + copyFields = table.concat( + ["_dst_.#{field} = _src_.#{field}" for field in *{...}] + "\n\t\t\t" + ) + | + do + local _src_, _dst_ + with _dst_ := #{dst} + with _src_ := #{src} + #{copyFields} export macro enum = (...) -> items = {...} diff --git a/spec/inputs/macro_teal.yue b/spec/inputs/macro_teal.yue index 0cfd862..e51bcd7 100644 --- a/spec/inputs/macro_teal.yue +++ b/spec/inputs/macro_teal.yue @@ -4,11 +4,16 @@ $ -> options.target_extension = "tl" package.path ..= ";./spec/lib/?.lua" -macro to_lua = (code)-> - "require('yue').to_lua(#{code}, reserve_line_number:false, same_module:true)" +macro to_lua = (code)-> | + require('yue').to_lua #{code}, + reserve_line_number: false + same_module: true -macro trim = (name)-> - "if result := #{name}\\match '[\\'\"](.*)[\\'\"]' then result else #{name}" +macro trim = (name)-> | + if result := #{name}\match '[\'"](.*)[\'"]' + result + else + #{name} export macro local = (decl, value = nil)-> import "yue" as {options:{:tl_enabled}} diff --git a/spec/inputs/macro_todo.yue b/spec/inputs/macro_todo.yue index 752c9cb..c9c8f77 100644 --- a/spec/inputs/macro_todo.yue +++ b/spec/inputs/macro_todo.yue @@ -5,9 +5,6 @@ export macro todoInner = (module, line, msg)-> type: "lua" } -export macro todo = (msg)-> - if msg - "$todoInner $FILE, $LINE, #{msg}" - else - "$todoInner $FILE, $LINE" +export macro todo = (msg)-> | + $todoInner $FILE, $LINE#{msg and ", #{msg}" or ""} diff --git a/spec/inputs/string.yue b/spec/inputs/string.yue index f91383e..1f0fba8 100644 --- a/spec/inputs/string.yue +++ b/spec/inputs/string.yue @@ -74,3 +74,76 @@ _ = "hello" something"hello"\world! something "hello"\world! +do + str = | + key: value + str = | + config: + enabled: true + level: 5 + str = | + header: start + + footer: end + str = | + name: #{username} + str = | + count: #{total} items + str = | + user: #{name} + id: #{id} + str = | + path: "C:\\Program Files\\App" + desc: 'single "quote" test' + str = | + key: value + next: 123 + str = | + list: + - "one" + - "two" + str = | + -- comment + content text + -- comment + str = | + #{1 + 2} + #{2 + 3} + #{"a" .. "b"} + obj = + settings: | + mode: #{mode} + flags: + - #{flag1} + - default + fn = -> | + Hello + name: #{userName} + str = | + result: + status: #{if ok then "pass" else "fail"} + code: #{code} + summary = | + date: #{os.date()} + values: + - + a: #{aVal} + b: #{bVal or defaultB} + msg = send | + Hello, #{user}! + Today is #{os.date("%A")}. + desc = do + prefix = "Result" + | + #{prefix}: + value: #{compute!} + (| + 1 + 2 + 3 + ) |> print + +export yaml = | + version: #{ver} + ok: true + diff --git a/spec/outputs/macro.lua b/spec/outputs/macro.lua index 9f5507c..89c6e63 100644 --- a/spec/outputs/macro.lua +++ b/spec/outputs/macro.lua @@ -26,6 +26,10 @@ print({ 123, 'xyz' }) +print({ + 456, + 'abc' +}) do assert(item == nil) end @@ -213,6 +217,13 @@ function tb:func() end end print(x) +local yue = require("yue") +do +local function f2(a) + return a + 1 +end +x = x + f2(3) +end local sel sel = function(a, b, c) if a then @@ -317,7 +328,7 @@ print((setmetatable({ return 998 end })) -print("current line: " .. tostring(323)) +print("current line: " .. tostring(349)) do do -- TODO diff --git a/spec/outputs/string.lua b/spec/outputs/string.lua index febea62..b536e6d 100644 --- a/spec/outputs/string.lua +++ b/spec/outputs/string.lua @@ -1,3 +1,4 @@ +local _module_0 = { } local hi = "hello" local hello = "what the heckyes" print(hi) @@ -41,4 +42,42 @@ local _ = "hello"; ("hello"):format().hello(1, 2, 3); ("hello"):format(1, 2, 3) something("hello"):world() -return something(("hello"):world()) +something(("hello"):world()) +do + local str = "key: value" + str = "config:\n\tenabled: true\n\tlevel: 5" + str = "header: start\nfooter: end" + str = "name: " .. tostring(username) + str = "count: " .. tostring(total) .. " items" + str = "user: " .. tostring(name) .. "\nid: " .. tostring(id) + str = "path: \"C:\\\\Program Files\\\\App\"\ndesc: 'single \"quote\" test'" + str = "key: value \nnext: 123 " + str = "list:\n - \"one\"\n - \"two\"" + str = "-- comment\ncontent text\n-- comment" + str = tostring(1 + 2) .. '\n' .. tostring(2 + 3) .. '\n' .. tostring("a" .. "b") + local obj = { + settings = "mode: " .. tostring(mode) .. "\nflags:\n\t- " .. tostring(flag1) .. "\n\t- default" + } + local fn + fn = function() + return "Hello\nname: " .. tostring(userName) + end + str = "result:\n\tstatus: " .. tostring((function() + if ok then + return "pass" + else + return "fail" + end + end)()) .. "\n\tcode: " .. tostring(code) + local summary = "date: " .. tostring(os.date()) .. "\nvalues:\n\t-\n\t\ta: " .. tostring(aVal) .. "\n\t\tb: " .. tostring(bVal or defaultB) + local msg = send("Hello, " .. tostring(user) .. "!\nToday is " .. tostring(os.date("%A")) .. ".") + local desc + do + local prefix = "Result" + desc = tostring(prefix) .. ":\nvalue: " .. tostring(compute()) + end + print(("1\n2\n3")) +end +local yaml = "version: " .. tostring(ver) .. "\nok: true" +_module_0["yaml"] = yaml +return _module_0 diff --git a/src/yuescript/yue_compiler.cpp b/src/yuescript/yue_compiler.cpp index 3768fc9..a8d222d 100644 --- a/src/yuescript/yue_compiler.cpp +++ b/src/yuescript/yue_compiler.cpp @@ -5440,7 +5440,7 @@ private: if (def->defaultValue) { defVal = _parser.toString(def->defaultValue); Utils::trim(defVal); - defVal = '=' + Utils::toLuaString(defVal); + defVal = '=' + Utils::toLuaDoubleString(defVal); } newArgs.emplace_back(_parser.toString(def->name) + defVal); } @@ -9185,15 +9185,34 @@ private: auto line = static_cast(line_); if (!line->segments.empty()) { str_list segs; + bool firstSeg = true; for (auto seg_ : line->segments.objects()) { auto content = static_cast(seg_)->content.get(); switch (content->get_id()) { case id(): { - auto str = _parser.toString(content); - Utils::replace(str, "\r\n"sv, "\n"sv); - Utils::replace(str, "\n"sv, "\\n"sv); - Utils::replace(str, "\\#"sv, "#"sv); - segs.push_back('\"' + str + '\"'); + auto seg = _parser.toString(content); + if (!indent) { + auto pos = seg.find_first_not_of("\t "sv); + if (pos == std::string::npos) { + indent = seg; + firstSeg = false; + continue; + } else { + indent = std::string{seg.c_str(), pos}; + } + } + if (firstSeg) { + firstSeg = false; + if (std::string_view{seg}.substr(0, indent.value().size()) != indent.value()) { + throw CompileError("inconsistent indent"sv, line); + } + auto seqStr = seg.substr(indent.value().size()); + if (!seqStr.empty()) { + segs.push_back(Utils::toLuaDoubleString(seqStr)); + } + } else { + segs.push_back(Utils::toLuaDoubleString(seg)); + } break; } case id(): { @@ -9204,20 +9223,7 @@ private: default: YUEE("AST node mismatch", content); break; } } - auto lineStr = join(segs, " .. "sv); - if (!indent) { - auto pos = lineStr.find_first_not_of("\t "sv, 1); - if (pos == std::string::npos) { - throw CompileError("expecting first line indent"sv, line); - } - indent = std::string{lineStr.c_str(), pos}; - } else { - if (std::string_view{lineStr}.substr(0, indent.value().size()) != indent.value()) { - throw CompileError("inconsistent indent"sv, line); - } - } - lineStr = '"' + lineStr.substr(indent.value().size()); - temp.push_back(lineStr); + temp.push_back(join(segs, " .. "sv)); } } auto str = join(temp, " .. '\\n' .. "sv); diff --git a/src/yuescript/yue_parser.cpp b/src/yuescript/yue_parser.cpp index eebc676..2e21a52 100644 --- a/src/yuescript/yue_parser.cpp +++ b/src/yuescript/yue_parser.cpp @@ -645,7 +645,7 @@ YueParser::YueParser() { YAMLLine = check_indent_match >> Seperator >> +YAMLLineContent | advance_match >> Seperator >> ensure(+YAMLLineContent, pop_indent) | Seperator >> *set(" \t") >> and_(line_break); - YAMLMultiline = '|' >> Seperator >> +space_break >> advance_match >> ensure(YAMLLine >> *(*set(" \t") >> line_break >> YAMLLine), pop_indent); + YAMLMultiline = '|' >> space >> Seperator >> +(*set(" \t") >> line_break) >> advance_match >> ensure(YAMLLine >> *(*set(" \t") >> line_break >> YAMLLine), pop_indent); String = DoubleString | SingleString | LuaString | YAMLMultiline; @@ -1185,7 +1185,7 @@ void trim(std::string& str) { str.erase(str.find_last_not_of(" \t\r\n") + 1); } -std::string toLuaString(const std::string& input) { +std::string toLuaDoubleString(const std::string& input) { std::string luaStr = "\""; for (char c : input) { switch (c) { diff --git a/src/yuescript/yue_parser.h b/src/yuescript/yue_parser.h index 15f9277..f4e0ab1 100644 --- a/src/yuescript/yue_parser.h +++ b/src/yuescript/yue_parser.h @@ -457,7 +457,7 @@ private: namespace Utils { void replace(std::string& str, std::string_view from, std::string_view to); void trim(std::string& str); -std::string toLuaString(const std::string& input); +std::string toLuaDoubleString(const std::string& input); } // namespace Utils } // namespace yue -- cgit v1.2.3-55-g6feb