From 1fe444e94cc501121a05a900362fc07da23d76cd Mon Sep 17 00:00:00 2001 From: uped Date: Fri, 6 Jun 2025 15:10:29 +0200 Subject: [PATCH 1/4] Pass comments to ast nodes during parsing --- spec/lang/parser/store_comments_spec.lua | 461 +++++++++++++++++++++++ tl.lua | 81 +++- tl.tl | 81 +++- 3 files changed, 589 insertions(+), 34 deletions(-) create mode 100644 spec/lang/parser/store_comments_spec.lua diff --git a/spec/lang/parser/store_comments_spec.lua b/spec/lang/parser/store_comments_spec.lua new file mode 100644 index 00000000..6ab0cd21 --- /dev/null +++ b/spec/lang/parser/store_comments_spec.lua @@ -0,0 +1,461 @@ +local tl = require("tl") +local util = require("spec.util") + +describe("store comments in syntax tree", function() + it("comments before implicit global function", function() + local result = tl.process_string([[ + -- this is a comment + function --[==[ignore me]==] foo() end + ]]) + assert.same({}, result.syntax_errors) + assert.same(1, #result.ast) + assert.same("statements", result.ast.kind) + assert.same("global_function", result.ast[1].kind) + assert.same("-- this is a comment", result.ast[1].comments[1].text) + end) + it("comments before local function", function() + local result = tl.process_string([[ + -- this is a comment + local function --[==[ignore me]==] foo() end + ]]) + assert.same({}, result.syntax_errors) + assert.same(1, #result.ast) + assert.same("statements", result.ast.kind) + assert.same("local_function", result.ast[1].kind) + assert.same("-- this is a comment", result.ast[1].comments[1].text) + end) + it("comments before local variable declaration", function() + local result = tl.process_string([[ + -- this is a comment + local --[==[ignore me]==] x = 42 + ]]) + assert.same({}, result.syntax_errors) + assert.same(1, #result.ast) + assert.same("statements", result.ast.kind) + assert.same("local_declaration", result.ast[1].kind) + assert.same("-- this is a comment", result.ast[1].comments[1].text) + end) + it("comments before local type declaration", function() + local result = tl.process_string([[ + -- this is a comment + local --[==[ignore me]==] type Foo = number + ]]) + assert.same({}, result.syntax_errors) + assert.same(1, #result.ast) + assert.same("statements", result.ast.kind) + assert.same("local_type", result.ast[1].kind) + assert.same("-- this is a comment", result.ast[1].comments[1].text) + end) + it("comments before local macroexp", function() + local result = tl.process_string([[ + -- this is a comment + local --[==[ignore me]==] macroexp foo(): number + return 2 + end + ]]) + assert.same({}, result.syntax_errors) + assert.same(1, #result.ast) + assert.same("statements", result.ast.kind) + assert.same("local_macroexp", result.ast[1].kind) + assert.same("-- this is a comment", result.ast[1].comments[1].text) + end) + it("comments before local record declaration", function() + local result = tl.process_string([[ + -- this is a comment + local --[==[ignore me]==] record Foo + x: number + end + ]]) + assert.same({}, result.syntax_errors) + assert.same(1, #result.ast) + assert.same("statements", result.ast.kind) + assert.same("local_type", result.ast[1].kind) + assert.same("-- this is a comment", result.ast[1].comments[1].text) + end) + it("comments before local enum declaration", function() + local result = tl.process_string([[ + -- this is a comment + local --[==[ignore me]==] enum Foo + "A" + "B" + end + ]]) + assert.same({}, result.syntax_errors) + assert.same(1, #result.ast) + assert.same("statements", result.ast.kind) + assert.same("local_type", result.ast[1].kind) + assert.same("-- this is a comment", result.ast[1].comments[1].text) + end) + it("comments before local interface declaration", function() + local result = tl.process_string([[ + -- this is a comment + local --[==[ignore me]==] interface Foo + x: number + end + ]]) + assert.same({}, result.syntax_errors) + assert.same(1, #result.ast) + assert.same("statements", result.ast.kind) + assert.same("local_type", result.ast[1].kind) + assert.same("-- this is a comment", result.ast[1].comments[1].text) + end) + it("comments before global function", function() + local result = tl.process_string([[ + -- this is a comment + global --[==[ignore me]==] function foo() end + ]]) + assert.same({}, result.syntax_errors) + assert.same(1, #result.ast) + assert.same("statements", result.ast.kind) + assert.same("global_function", result.ast[1].kind) + assert.same("-- this is a comment", result.ast[1].comments[1].text) + end) + it("comments before global variable declaration", function() + local result = tl.process_string([[ + -- this is a comment + global --[==[ignore me]==] x = 42 + ]]) + assert.same({}, result.syntax_errors) + assert.same(1, #result.ast) + assert.same("statements", result.ast.kind) + assert.same("global_declaration", result.ast[1].kind) + assert.same("-- this is a comment", result.ast[1].comments[1].text) + end) + it("comments before global type declaration", function() + local result = tl.process_string([[ + -- this is a comment + global --[==[ignore me]==] type Foo = number + ]]) + assert.same({}, result.syntax_errors) + assert.same(1, #result.ast) + assert.same("statements", result.ast.kind) + assert.same("global_type", result.ast[1].kind) + assert.same("-- this is a comment", result.ast[1].comments[1].text) + end) + it("comments before global record declaration", function() + local result = tl.process_string([[ + -- this is a comment + global --[==[ignore me]==] record Foo + x: number + end + ]]) + assert.same({}, result.syntax_errors) + assert.same(1, #result.ast) + assert.same("statements", result.ast.kind) + assert.same("global_type", result.ast[1].kind) + assert.same("-- this is a comment", result.ast[1].comments[1].text) + end) + it("comments before global enum declaration", function() + local result = tl.process_string([[ + -- this is a comment + global --[==[ignore me]==] enum Foo + "A" + "B" + end + ]]) + assert.same({}, result.syntax_errors) + assert.same(1, #result.ast) + assert.same("statements", result.ast.kind) + assert.same("global_type", result.ast[1].kind) + assert.same("-- this is a comment", result.ast[1].comments[1].text) + end) + it("comments before global interface declaration", function() + local result = tl.process_string([[ + -- this is a comment + global --[==[ignore me]==] interface Foo + x: number + end + ]]) + assert.same({}, result.syntax_errors) + assert.same(1, #result.ast) + assert.same("statements", result.ast.kind) + assert.same("global_type", result.ast[1].kind) + assert.same("-- this is a comment", result.ast[1].comments[1].text) + end) + it("comments before record fields", function() + local result = tl.process_string([[ + local record Foo + -- this is a comment + x: number + -- another comment + y: string + end + ]]) + assert.same({}, result.syntax_errors) + assert.same(1, #result.ast) + assert.same("statements", result.ast.kind) + assert.same("local_type", result.ast[1].kind) + assert.same("newtype", result.ast[1].value.kind) + local record_def = result.ast[1].value.newtype.def + assert.same("record", record_def.typename) + local expected_comments = { + x = "-- this is a comment", + y = "-- another comment" + } + for field_name, _ in pairs(record_def.fields) do + assert.same(expected_comments[field_name], record_def.field_comments[field_name][1][1].text) + end + end) + it("comments before record type fields", function() + local result = tl.process_string([[ + local record Foo + -- this is a comment + type x = number + -- another comment + type y = string + end + ]]) + assert.same({}, result.syntax_errors) + assert.same(1, #result.ast) + assert.same("statements", result.ast.kind) + assert.same("local_type", result.ast[1].kind) + assert.same("newtype", result.ast[1].value.kind) + local record_def = result.ast[1].value.newtype.def + assert.same("record", record_def.typename) + local expected_comments = { + x = "-- this is a comment", + y = "-- another comment" + } + for field_name, _ in pairs(record_def.fields) do + assert.same(expected_comments[field_name], record_def.field_comments[field_name][1][1].text) + end + end) + it("comments before record nested declarations", function() + local result = tl.process_string([[ + local record Foo + -- this is a comment + record Bar + x: number + end + -- another comment + interface Baz + y: string + end + -- yet another comment + enum Qux + "A" + "B" + end + end + ]]) + assert.same({}, result.syntax_errors) + assert.same(1, #result.ast) + assert.same("statements", result.ast.kind) + assert.same("local_type", result.ast[1].kind) + assert.same("newtype", result.ast[1].value.kind) + local record_def = result.ast[1].value.newtype.def + assert.same("record", record_def.typename) + local expected_comments = { + Bar = "-- this is a comment", + Baz = "-- another comment", + Qux = "-- yet another comment" + } + for field_name, _ in pairs(record_def.fields) do + assert.same(expected_comments[field_name], record_def.field_comments[field_name][1][1].text) + end + end) + it("comments before record metafields", function() + local result = tl.process_string([[ + local record Foo + -- this is a comment + metamethod __call: function(Foo, string, number): string + -- another comment + metamethod __add: function(Foo, Foo): Foo + end + ]]) + assert.same({}, result.syntax_errors) + assert.same(1, #result.ast) + assert.same("statements", result.ast.kind) + assert.same("local_type", result.ast[1].kind) + assert.same("newtype", result.ast[1].value.kind) + local record_def = result.ast[1].value.newtype.def + assert.same("record", record_def.typename) + local expected_comments = { + ["__call"] = "-- this is a comment", + ["__add"] = "-- another comment", + } + for field_name, _ in pairs(record_def.fields) do + assert.same(expected_comments[field_name], record_def.meta_field_comments[field_name][1][1].text) + end + end) + it("comments before record overloaded functions", function() + local result = tl.process_string([[ + local record Foo + -- this is a comment + bar: function(string): string + -- another comment + bar: function(number): number + end + ]]) + assert.same({}, result.syntax_errors) + assert.same(1, #result.ast) + assert.same("statements", result.ast.kind) + assert.same("local_type", result.ast[1].kind) + assert.same("newtype", result.ast[1].value.kind) + local record_def = result.ast[1].value.newtype.def + assert.same("record", record_def.typename) + local expected_comments = { + ["bar"] = {"-- this is a comment", "-- another comment"} + } + local i = 1 + for field_name, _ in pairs(record_def.fields) do + assert.same(expected_comments[field_name][i], record_def.field_comments[field_name][i][1].text) + i = i + 1 + end + end) + it("comments before interface fields", function() + local result = tl.process_string([[ + local interface Foo + -- this is a comment + x: number + -- another comment + y: string + end + ]]) + assert.same({}, result.syntax_errors) + assert.same(1, #result.ast) + assert.same("statements", result.ast.kind) + assert.same("local_type", result.ast[1].kind) + assert.same("newtype", result.ast[1].value.kind) + local interface_def = result.ast[1].value.newtype.def + assert.same("interface", interface_def.typename) + local expected_comments = { + x = "-- this is a comment", + y = "-- another comment" + } + for field_name, _ in pairs(interface_def.fields) do + assert.same(expected_comments[field_name], interface_def.field_comments[field_name][1][1].text) + end + end) + it("comments before interface type fields", function() + local result = tl.process_string([[ + local interface Foo + -- this is a comment + type x = number + -- another comment + type y = string + end + ]]) + assert.same({}, result.syntax_errors) + assert.same(1, #result.ast) + assert.same("statements", result.ast.kind) + assert.same("local_type", result.ast[1].kind) + assert.same("newtype", result.ast[1].value.kind) + local interface_def = result.ast[1].value.newtype.def + assert.same("interface", interface_def.typename) + local expected_comments = { + x = "-- this is a comment", + y = "-- another comment" + } + for field_name, _ in pairs(interface_def.fields) do + assert.same(expected_comments[field_name], interface_def.field_comments[field_name][1][1].text) + end + end) + it("comments before interface nested declarations", function() + local result = tl.process_string([[ + local interface Foo + -- this is a comment + interface Bar + x: number + end + -- another comment + interface Baz + y: string + end + -- yet another comment + enum Qux + "A" + "B" + end + end + ]]) + assert.same({}, result.syntax_errors) + assert.same(1, #result.ast) + assert.same("statements", result.ast.kind) + assert.same("local_type", result.ast[1].kind) + assert.same("newtype", result.ast[1].value.kind) + local interface_def = result.ast[1].value.newtype.def + assert.same("interface", interface_def.typename) + local expected_comments = { + Bar = "-- this is a comment", + Baz = "-- another comment", + Qux = "-- yet another comment" + } + for field_name, _ in pairs(interface_def.fields) do + assert.same(expected_comments[field_name], interface_def.field_comments[field_name][1][1].text) + end + end) + it("comments before interface metafields", function() + local result = tl.process_string([[ + local interface Foo + -- this is a comment + metamethod __call: function(Foo, string, number): string + -- another comment + metamethod __add: function(Foo, Foo): Foo + end + ]]) + assert.same({}, result.syntax_errors) + assert.same(1, #result.ast) + assert.same("statements", result.ast.kind) + assert.same("local_type", result.ast[1].kind) + assert.same("newtype", result.ast[1].value.kind) + local interface_def = result.ast[1].value.newtype.def + assert.same("interface", interface_def.typename) + local expected_comments = { + ["__call"] = "-- this is a comment", + ["__add"] = "-- another comment", + } + for field_name, _ in pairs(interface_def.fields) do + assert.same(expected_comments[field_name], interface_def.meta_field_comments[field_name][1][1].text) + end + end) + it("comments before interface overloaded functions", function() + local result = tl.process_string([[ + local interface Foo + -- this is a comment + bar: function(string): string + -- another comment + bar: function(number): number + end + ]]) + assert.same({}, result.syntax_errors) + assert.same(1, #result.ast) + assert.same("statements", result.ast.kind) + assert.same("local_type", result.ast[1].kind) + assert.same("newtype", result.ast[1].value.kind) + local interface_def = result.ast[1].value.newtype.def + assert.same("interface", interface_def.typename) + local expected_comments = { + ["bar"] = {"-- this is a comment", "-- another comment"} + } + local i = 1 + for field_name, _ in pairs(interface_def.fields) do + assert.same(expected_comments[field_name][i], interface_def.field_comments[field_name][i][1].text) + i = i + 1 + end + end) + it("comments before enum values", function() + local result = tl.process_string([[ + local enum Foo + -- this is a comment + "A" + -- another comment + "B" + end + ]]) + assert.same({}, result.syntax_errors) + assert.same(1, #result.ast) + assert.same("statements", result.ast.kind) + assert.same("local_type", result.ast[1].kind) + assert.same("newtype", result.ast[1].value.kind) + local enum_def = result.ast[1].value.newtype.def + assert.same("enum", enum_def.typename) + local expected_comments = { + ["A"] = "-- this is a comment", + ["B"] = "-- another comment" + } + for _, value_name in ipairs(enum_def.enumset) do + assert.same(expected_comments[value_name], enum_def.value_comments[value_name][1].text) + end + end) +end) \ No newline at end of file diff --git a/tl.lua b/tl.lua index 70c95861..adc7eecf 100644 --- a/tl.lua +++ b/tl.lua @@ -2092,6 +2092,10 @@ end + + + + @@ -2283,6 +2287,7 @@ local Node = { ExpectedContext = {} } + local show_type @@ -3853,13 +3858,23 @@ do return i, node end - local function store_field_in_record(ps, i, field_name, newt, fields, field_order) + local function store_field_in_record(ps, i, field_name, newt, comments, fields, field_order, field_comments) if not fields[field_name] then fields[field_name] = newt + if comments then + field_comments[field_name] = { comments } + end table.insert(field_order, field_name) return true end + if comments then + if not field_comments[field_name] then + field_comments[field_name] = {} + end + table.insert(field_comments[field_name], comments) + end + local oldt = fields[field_name] local oldf = oldt.typename == "generic" and oldt.t or oldt local newf = newt.typename == "generic" and newt.t or newt @@ -3914,17 +3929,20 @@ do nt.newtype = new_typedecl(ps, istart, ndef) - store_field_in_record(ps, iv, v.tk, nt.newtype, def.fields, def.field_order) + store_field_in_record(ps, iv, v.tk, nt.newtype, ps.tokens[istart].comments, def.fields, def.field_order, def.field_comments) return i end parse_enum_body = function(ps, i, def) def.enumset = {} + def.value_comments = {} while ps.tokens[i].tk ~= "$EOF$" and ps.tokens[i].tk ~= "end" do local item i, item = verify_kind(ps, i, "string", "string") if item then - def.enumset[unquote(item.tk)] = true + local name = unquote(item.tk) + def.enumset[name] = true + def.value_comments[name] = item.comments end end return i, true @@ -4053,6 +4071,7 @@ do parse_record_body = function(ps, i, def) def.fields = {} def.field_order = {} + def.field_comments = {} if ps.tokens[i].tk == "{" then local atype @@ -4104,7 +4123,8 @@ do def.meta_fields = {} def.meta_field_order = {} - store_field_in_record(ps, i, "__is", typ, def.meta_fields, def.meta_field_order) + def.meta_field_comments = {} + store_field_in_record(ps, i, "__is", typ, {}, def.meta_fields, def.meta_field_order, def.meta_field_comments) end while not (ps.tokens[i].kind == "$EOF$" or ps.tokens[i].tk == "end") do @@ -4119,6 +4139,7 @@ do elseif ps.tokens[i].tk == "{" then return fail(ps, i, "syntax error: this syntax is no longer valid; declare array interface at the top with 'is {...}'") elseif ps.tokens[i].tk == "type" and ps.tokens[i + 1].tk ~= ":" then + local comments = ps.tokens[i].comments i = i + 1 local iv = i @@ -4143,7 +4164,7 @@ do ntt.is_nested_alias = true end - store_field_in_record(ps, iv, v.tk, nt.newtype, def.fields, def.field_order) + store_field_in_record(ps, iv, v.tk, nt.newtype, comments, def.fields, def.field_order, def.field_comments) elseif parse_type_body_fns[tn] and ps.tokens[i + 1].tk ~= ":" then if def.typename == "interface" and tn == "record" then i = failskip(ps, i, "interfaces cannot contain record definitions", skip_type_body) @@ -4151,6 +4172,7 @@ do i = parse_nested_type(ps, i, def, tn) end else + local comments = ps.tokens[i].comments local is_metamethod = false if ps.tokens[i].tk == "metamethod" and ps.tokens[i + 1].tk ~= ":" then is_metamethod = true @@ -4186,13 +4208,16 @@ do local field_name = v.conststr or v.tk local fields = def.fields local field_order = def.field_order + local field_comments = def.field_comments if is_metamethod then if not def.meta_fields then def.meta_fields = {} def.meta_field_order = {} + def.meta_field_comments = {} end fields = def.meta_fields field_order = def.meta_field_order + field_comments = def.meta_field_comments if not metamethod_names[field_name] then fail(ps, i - 1, "not a valid metamethod: " .. field_name) end @@ -4208,7 +4233,7 @@ do end end - store_field_in_record(ps, iv, field_name, t, fields, field_order) + store_field_in_record(ps, iv, field_name, t, comments, fields, field_order, field_comments) elseif ps.tokens[i].tk == "=" then local next_word = ps.tokens[i + 1].tk if next_word == "record" or next_word == "enum" then @@ -4480,37 +4505,59 @@ do end local function parse_local(ps, i) + local comments = ps.tokens[i].comments local ntk = ps.tokens[i + 1].tk local tn = ntk + + local node if ntk == "function" then - return parse_local_function(ps, i) + i, node = parse_local_function(ps, i) elseif ntk == "type" and ps.tokens[i + 2].kind == "identifier" then - return parse_type_declaration(ps, i + 2, "local_type") + i, node = parse_type_declaration(ps, i + 2, "local_type") elseif ntk == "macroexp" and ps.tokens[i + 2].kind == "identifier" then - return parse_local_macroexp(ps, i) + i, node = parse_local_macroexp(ps, i) elseif parse_type_body_fns[tn] and ps.tokens[i + 2].kind == "identifier" then - return parse_type_constructor(ps, i, "local_type", tn) + i, node = parse_type_constructor(ps, i, "local_type", tn) + else + i, node = parse_variable_declarations(ps, i + 1, "local_declaration") + end + if node then + node.comments = comments end - return parse_variable_declarations(ps, i + 1, "local_declaration") + return i, node end local function parse_global(ps, i) + local comments = ps.tokens[i].comments local ntk = ps.tokens[i + 1].tk local tn = ntk + + local node if ntk == "function" then - return parse_function(ps, i + 1, "global") + i, node = parse_function(ps, i + 1, "global") elseif ntk == "type" and ps.tokens[i + 2].kind == "identifier" then - return parse_type_declaration(ps, i + 2, "global_type") + i, node = parse_type_declaration(ps, i + 2, "global_type") elseif parse_type_body_fns[tn] and ps.tokens[i + 2].kind == "identifier" then - return parse_type_constructor(ps, i, "global_type", tn) + i, node = parse_type_constructor(ps, i, "global_type", tn) elseif ps.tokens[i + 1].kind == "identifier" then - return parse_variable_declarations(ps, i + 1, "global_declaration") + i, node = parse_variable_declarations(ps, i + 1, "global_declaration") + else + return parse_call_or_assignment(ps, i) end - return parse_call_or_assignment(ps, i) + if node then + node.comments = comments + end + return i, node end local function parse_record_function(ps, i) - return parse_function(ps, i, "record") + local comments = ps.tokens[i].comments + local node + i, node = parse_function(ps, i, "record") + if node then + node.comments = comments + end + return i, node end local function parse_pragma(ps, i) diff --git a/tl.tl b/tl.tl index 642aa2e1..1327750d 100644 --- a/tl.tl +++ b/tl.tl @@ -1883,8 +1883,10 @@ local interface RecordLikeType interfaces_expanded: boolean fields: {string: Type} field_order: {string} + field_comments: {string: {{Comment}}} meta_fields: {string: Type} meta_field_order: {string} + meta_field_comments: {string: {{Comment}}} is_userdata: boolean end @@ -2010,6 +2012,7 @@ end -- Intersection types, currently restricted to polymorphic functions -- defined inside records, representing polymorphic Lua APIs. +-- types order should match the order of declarations in the record or interface. local record PolyType is AggregateType where self.typename == "poly" @@ -2022,6 +2025,7 @@ local record EnumType where self.typename == "enum" enumset: {string:boolean} + value_comments: {string:{Comment}} end local record Operator @@ -2183,6 +2187,7 @@ local record Node tk: string kind: NodeKind + comments: {Comment} symbol_list_slot: integer semicolon: boolean hashbang: string @@ -3853,13 +3858,23 @@ local function parse_return(ps: ParseState, i: integer): integer, Node return i, node end -local function store_field_in_record(ps: ParseState, i: integer, field_name: string, newt: Type, fields: {string: Type}, field_order: {string}): boolean +local function store_field_in_record(ps: ParseState, i: integer, field_name: string, newt: Type, comments: {Comment}, fields: {string: Type}, field_order: {string}, field_comments: {string: {{Comment}}}): boolean if not fields[field_name] then fields[field_name] = newt + if comments then + field_comments[field_name] = {comments} + end table.insert(field_order, field_name) return true end + if comments then + if not field_comments[field_name] then + field_comments[field_name] = {} + end + table.insert(field_comments[field_name], comments) + end + local oldt = fields[field_name] local oldf = oldt is GenericType and oldt.t or oldt local newf = newt is GenericType and newt.t or newt @@ -3914,17 +3929,20 @@ local function parse_nested_type(ps: ParseState, i: integer, def: RecordLikeType nt.newtype = new_typedecl(ps, istart, ndef) - store_field_in_record(ps, iv, v.tk, nt.newtype, def.fields, def.field_order) + store_field_in_record(ps, iv, v.tk, nt.newtype, ps.tokens[istart].comments, def.fields, def.field_order, def.field_comments) return i end parse_enum_body = function(ps: ParseState, i: integer, def: EnumType): integer, boolean def.enumset = {} + def.value_comments = {} while ps.tokens[i].tk ~= "$EOF$" and ps.tokens[i].tk ~= "end" do local item: Node i, item = verify_kind(ps, i, "string", "string") if item then - def.enumset[unquote(item.tk)] = true + local name = unquote(item.tk) + def.enumset[name] = true + def.value_comments[name] = item.comments end end return i, true @@ -4053,6 +4071,7 @@ end parse_record_body = function(ps: ParseState, i: integer, def: RecordLikeType): integer, boolean def.fields = {} def.field_order = {} + def.field_comments = {} if ps.tokens[i].tk == "{" then local atype: Type @@ -4104,7 +4123,8 @@ parse_record_body = function(ps: ParseState, i: integer, def: RecordLikeType): i def.meta_fields = {} def.meta_field_order = {} - store_field_in_record(ps, i, "__is", typ, def.meta_fields, def.meta_field_order) + def.meta_field_comments = {} + store_field_in_record(ps, i, "__is", typ, {}, def.meta_fields, def.meta_field_order, def.meta_field_comments) end while not (ps.tokens[i].kind == "$EOF$" or ps.tokens[i].tk == "end") do @@ -4119,6 +4139,7 @@ parse_record_body = function(ps: ParseState, i: integer, def: RecordLikeType): i elseif ps.tokens[i].tk == "{" then return fail(ps, i, "syntax error: this syntax is no longer valid; declare array interface at the top with 'is {...}'") elseif ps.tokens[i].tk == "type" and ps.tokens[i + 1].tk ~= ":" then + local comments = ps.tokens[i].comments i = i + 1 local iv = i @@ -4143,7 +4164,7 @@ parse_record_body = function(ps: ParseState, i: integer, def: RecordLikeType): i ntt.is_nested_alias = true end - store_field_in_record(ps, iv, v.tk, nt.newtype, def.fields, def.field_order) + store_field_in_record(ps, iv, v.tk, nt.newtype, comments, def.fields, def.field_order, def.field_comments) elseif parse_type_body_fns[tn] and ps.tokens[i+1].tk ~= ":" then if def.typename == "interface" and tn == "record" then i = failskip(ps, i, "interfaces cannot contain record definitions", skip_type_body) @@ -4151,6 +4172,7 @@ parse_record_body = function(ps: ParseState, i: integer, def: RecordLikeType): i i = parse_nested_type(ps, i, def, tn) end else + local comments = ps.tokens[i].comments local is_metamethod = false if ps.tokens[i].tk == "metamethod" and ps.tokens[i+1].tk ~= ":" then is_metamethod = true @@ -4186,13 +4208,16 @@ parse_record_body = function(ps: ParseState, i: integer, def: RecordLikeType): i local field_name = v.conststr or v.tk local fields = def.fields local field_order = def.field_order + local field_comments = def.field_comments if is_metamethod then if not def.meta_fields then def.meta_fields = {} def.meta_field_order = {} + def.meta_field_comments = {} end fields = def.meta_fields field_order = def.meta_field_order + field_comments = def.meta_field_comments if not metamethod_names[field_name] then fail(ps, i - 1, "not a valid metamethod: " .. field_name) end @@ -4208,7 +4233,7 @@ parse_record_body = function(ps: ParseState, i: integer, def: RecordLikeType): i end end - store_field_in_record(ps, iv, field_name, t, fields, field_order) + store_field_in_record(ps, iv, field_name, t, comments, fields, field_order, field_comments) elseif ps.tokens[i].tk == "=" then local next_word = ps.tokens[i + 1].tk if next_word == "record" or next_word == "enum" then @@ -4480,37 +4505,59 @@ local function parse_local_macroexp(ps: ParseState, i: integer): integer, Node end local function parse_local(ps: ParseState, i: integer): integer, Node + local comments = ps.tokens[i].comments local ntk = ps.tokens[i + 1].tk local tn = ntk as TypeName + + local node: Node if ntk == "function" then - return parse_local_function(ps, i) + i, node = parse_local_function(ps, i) elseif ntk == "type" and ps.tokens[i + 2].kind == "identifier" then - return parse_type_declaration(ps, i + 2, "local_type") + i, node = parse_type_declaration(ps, i + 2, "local_type") elseif ntk == "macroexp" and ps.tokens[i+2].kind == "identifier" then - return parse_local_macroexp(ps, i) + i, node = parse_local_macroexp(ps, i) elseif parse_type_body_fns[tn as BodyTypeName] and ps.tokens[i+2].kind == "identifier" then - return parse_type_constructor(ps, i, "local_type", tn as BodyTypeName) + i, node = parse_type_constructor(ps, i, "local_type", tn as BodyTypeName) + else + i, node = parse_variable_declarations(ps, i + 1, "local_declaration") end - return parse_variable_declarations(ps, i + 1, "local_declaration") + if node then + node.comments = comments + end + return i, node end local function parse_global(ps: ParseState, i: integer): integer, Node + local comments = ps.tokens[i].comments local ntk = ps.tokens[i + 1].tk local tn = ntk as TypeName + + local node: Node if ntk == "function" then - return parse_function(ps, i + 1, "global") + i, node = parse_function(ps, i + 1, "global") elseif ntk == "type" and ps.tokens[i + 2].kind == "identifier" then - return parse_type_declaration(ps, i + 2, "global_type") + i, node = parse_type_declaration(ps, i + 2, "global_type") elseif parse_type_body_fns[tn as BodyTypeName] and ps.tokens[i+2].kind == "identifier" then - return parse_type_constructor(ps, i, "global_type", tn as BodyTypeName) + i, node = parse_type_constructor(ps, i, "global_type", tn as BodyTypeName) elseif ps.tokens[i+1].kind == "identifier" then - return parse_variable_declarations(ps, i + 1, "global_declaration") + i, node = parse_variable_declarations(ps, i + 1, "global_declaration") + else + return parse_call_or_assignment(ps, i) -- global is a soft keyword end - return parse_call_or_assignment(ps, i) -- global is a soft keyword + if node then + node.comments = comments + end + return i, node end local function parse_record_function(ps: ParseState, i: integer): integer, Node - return parse_function(ps, i, "record") + local comments = ps.tokens[i].comments + local node: Node + i, node = parse_function(ps, i, "record") + if node then + node.comments = comments + end + return i, node end local function parse_pragma(ps: ParseState, i: integer): integer, Node From 500aa47be98b3d52bc10eb927633ddfca41ab9a9 Mon Sep 17 00:00:00 2001 From: uped Date: Mon, 9 Jun 2025 18:14:01 +0200 Subject: [PATCH 2/4] fix issues from code review --- spec/lang/parser/store_comments_spec.lua | 49 ++++++++++++++--- tl.lua | 63 +++++++++++++--------- tl.tl | 67 ++++++++++++++---------- 3 files changed, 121 insertions(+), 58 deletions(-) diff --git a/spec/lang/parser/store_comments_spec.lua b/spec/lang/parser/store_comments_spec.lua index 6ab0cd21..6627668a 100644 --- a/spec/lang/parser/store_comments_spec.lua +++ b/spec/lang/parser/store_comments_spec.lua @@ -297,10 +297,14 @@ describe("store comments in syntax tree", function() local expected_comments = { ["bar"] = {"-- this is a comment", "-- another comment"} } - local i = 1 for field_name, _ in pairs(record_def.fields) do - assert.same(expected_comments[field_name][i], record_def.field_comments[field_name][i][1].text) - i = i + 1 + for i = 1, #record_def.field_comments[field_name] do + if not expected_comments[field_name][i] then + assert.same({}, record_def.field_comments[field_name][i]) + else + assert.same(expected_comments[field_name][i], record_def.field_comments[field_name][i][1].text) + end + end end end) it("comments before interface fields", function() @@ -428,10 +432,14 @@ describe("store comments in syntax tree", function() local expected_comments = { ["bar"] = {"-- this is a comment", "-- another comment"} } - local i = 1 for field_name, _ in pairs(interface_def.fields) do - assert.same(expected_comments[field_name][i], interface_def.field_comments[field_name][i][1].text) - i = i + 1 + for i = 1, #interface_def.field_comments[field_name] do + if not expected_comments[field_name][i] then + assert.same({}, interface_def.field_comments[field_name][i]) + else + assert.same(expected_comments[field_name][i], interface_def.field_comments[field_name][i][1].text) + end + end end end) it("comments before enum values", function() @@ -458,4 +466,33 @@ describe("store comments in syntax tree", function() assert.same(expected_comments[value_name], enum_def.value_comments[value_name][1].text) end end) + it("comments attach to the correct entry in polymorphic function", function() + local result = tl.process_string([[ + local record MyRecord + f: function(integer) + --- it can be a boolean too + f: function(boolean) + f: function(number) + end + ]]) + assert.same({}, result.syntax_errors) + assert.same(1, #result.ast) + assert.same("statements", result.ast.kind) + assert.same("local_type", result.ast[1].kind) + assert.same("newtype", result.ast[1].value.kind) + local record_def = result.ast[1].value.newtype.def + assert.same("record", record_def.typename) + local expected_comments = { + ["f"] = {nil, "--- it can be a boolean too", nil} + } + for field_name, _ in pairs(record_def.fields) do + for i = 1, #record_def.field_comments[field_name] do + if not expected_comments[field_name][i] then + assert.same({}, record_def.field_comments[field_name][i]) + else + assert.same(expected_comments[field_name][i], record_def.field_comments[field_name][i][1].text) + end + end + end + end) end) \ No newline at end of file diff --git a/tl.lua b/tl.lua index adc7eecf..63eb8574 100644 --- a/tl.lua +++ b/tl.lua @@ -2098,7 +2098,6 @@ end - local TruthyFact = {} @@ -2455,6 +2454,10 @@ end + + + + do @@ -3858,7 +3861,18 @@ do return i, node end - local function store_field_in_record(ps, i, field_name, newt, comments, fields, field_order, field_comments) + local function store_field_in_record(ps, i, field_name, newt, def, comments, meta) + local field_order, fields, field_comments + if meta then + field_order, fields, field_comments = def.meta_field_order, def.meta_fields, def.meta_field_comments + else + field_order, fields, field_comments = def.field_order, def.fields, def.field_comments + end + + if comments and not field_comments then + field_comments = {} + end + if not fields[field_name] then fields[field_name] = newt if comments then @@ -3868,25 +3882,34 @@ do return true end - if comments then - if not field_comments[field_name] then - field_comments[field_name] = {} - end - table.insert(field_comments[field_name], comments) - end - local oldt = fields[field_name] local oldf = oldt.typename == "generic" and oldt.t or oldt local newf = newt.typename == "generic" and newt.t or newt + local function store_comment_for_poly(poly) + if comments then + if not field_comments[field_name] then + field_comments[field_name] = {} + for idx = 1, #poly.types - 1 do + field_comments[field_name][idx] = {} + end + end + table.insert(field_comments[field_name], comments) + elseif field_comments[field_name] then + table.insert(field_comments[field_name], {}) + end + end + if newf.typename == "function" then if oldf.typename == "function" then local p = new_type(ps, i, "poly") p.types = { oldt, newt } fields[field_name] = p + store_comment_for_poly(p) return true elseif oldt.typename == "poly" then table.insert(oldt.types, newt) + store_comment_for_poly(oldt) return true end end @@ -3929,7 +3952,7 @@ do nt.newtype = new_typedecl(ps, istart, ndef) - store_field_in_record(ps, iv, v.tk, nt.newtype, ps.tokens[istart].comments, def.fields, def.field_order, def.field_comments) + store_field_in_record(ps, iv, v.tk, nt.newtype, def, ps.tokens[istart].comments) return i end @@ -4123,8 +4146,7 @@ do def.meta_fields = {} def.meta_field_order = {} - def.meta_field_comments = {} - store_field_in_record(ps, i, "__is", typ, {}, def.meta_fields, def.meta_field_order, def.meta_field_comments) + store_field_in_record(ps, i, "__is", typ, def, nil, "meta") end while not (ps.tokens[i].kind == "$EOF$" or ps.tokens[i].tk == "end") do @@ -4164,7 +4186,7 @@ do ntt.is_nested_alias = true end - store_field_in_record(ps, iv, v.tk, nt.newtype, comments, def.fields, def.field_order, def.field_comments) + store_field_in_record(ps, iv, v.tk, nt.newtype, def, comments) elseif parse_type_body_fns[tn] and ps.tokens[i + 1].tk ~= ":" then if def.typename == "interface" and tn == "record" then i = failskip(ps, i, "interfaces cannot contain record definitions", skip_type_body) @@ -4206,18 +4228,11 @@ do end local field_name = v.conststr or v.tk - local fields = def.fields - local field_order = def.field_order - local field_comments = def.field_comments if is_metamethod then if not def.meta_fields then def.meta_fields = {} def.meta_field_order = {} - def.meta_field_comments = {} end - fields = def.meta_fields - field_order = def.meta_field_order - field_comments = def.meta_field_comments if not metamethod_names[field_name] then fail(ps, i - 1, "not a valid metamethod: " .. field_name) end @@ -4233,7 +4248,7 @@ do end end - store_field_in_record(ps, iv, field_name, t, comments, fields, field_order, field_comments) + store_field_in_record(ps, iv, field_name, t, def, comments, is_metamethod and "meta" or nil) elseif ps.tokens[i].tk == "=" then local next_word = ps.tokens[i + 1].tk if next_word == "record" or next_word == "enum" then @@ -4709,10 +4724,6 @@ end - - - - @@ -8170,6 +8181,7 @@ do copy.fields = {} copy.field_order = {} + copy.field_comments = t.field_comments for i, k in ipairs(t.field_order) do copy.field_order[i] = k copy.fields[k], same = resolve(t.fields[k], same) @@ -8178,6 +8190,7 @@ do if t.meta_fields then copy.meta_fields = {} copy.meta_field_order = {} + copy.meta_field_comments = t.meta_field_comments for i, k in ipairs(t.meta_field_order) do copy.meta_field_order[i] = k copy.meta_fields[k], same = resolve(t.meta_fields[k], same) diff --git a/tl.tl b/tl.tl index 1327750d..e6bdfae7 100644 --- a/tl.tl +++ b/tl.tl @@ -2012,12 +2012,11 @@ end -- Intersection types, currently restricted to polymorphic functions -- defined inside records, representing polymorphic Lua APIs. --- types order should match the order of declarations in the record or interface. local record PolyType is AggregateType where self.typename == "poly" - types: {FunctionType | GenericType} + types: {FunctionType | GenericType} -- order should match the order of declarations in the record or interface. end local record EnumType @@ -2455,6 +2454,10 @@ local enum ParseLang "tl" end +local enum MetaMode + "meta" +end + do ----------------------------------------------------------------------------- local record ParseState @@ -3858,35 +3861,55 @@ local function parse_return(ps: ParseState, i: integer): integer, Node return i, node end -local function store_field_in_record(ps: ParseState, i: integer, field_name: string, newt: Type, comments: {Comment}, fields: {string: Type}, field_order: {string}, field_comments: {string: {{Comment}}}): boolean +local function store_field_in_record(ps: ParseState, i: integer, field_name: string, newt: Type, def: RecordLikeType, comments?: {Comment}, meta?: MetaMode): boolean + local field_order, fields, field_comments: {string}, {string:Type}, {string: {{Comment}}} + if meta then + field_order, fields, field_comments = def.meta_field_order, def.meta_fields, def.meta_field_comments + else + field_order, fields, field_comments = def.field_order, def.fields, def.field_comments + end + + if comments and not field_comments then + field_comments = {} + end + if not fields[field_name] then fields[field_name] = newt - if comments then + if comments then field_comments[field_name] = {comments} end table.insert(field_order, field_name) return true end - if comments then - if not field_comments[field_name] then - field_comments[field_name] = {} - end - table.insert(field_comments[field_name], comments) - end - local oldt = fields[field_name] local oldf = oldt is GenericType and oldt.t or oldt local newf = newt is GenericType and newt.t or newt + local function store_comment_for_poly(poly: PolyType) + if comments then + if not field_comments[field_name] then + field_comments[field_name] = {} + for idx = 1, #poly.types - 1 do -- padding for existing types + field_comments[field_name][idx] = {} + end + end + table.insert(field_comments[field_name], comments) + elseif field_comments[field_name] then + table.insert(field_comments[field_name], {}) + end + end + if newf is FunctionType then if oldf is FunctionType then local p = new_type(ps, i, "poly") as PolyType p.types = { oldt as FunctionType, newt as FunctionType } fields[field_name] = p + store_comment_for_poly(p) return true elseif oldt is PolyType then table.insert(oldt.types, newt as FunctionType) + store_comment_for_poly(oldt) return true end end @@ -3929,7 +3952,7 @@ local function parse_nested_type(ps: ParseState, i: integer, def: RecordLikeType nt.newtype = new_typedecl(ps, istart, ndef) - store_field_in_record(ps, iv, v.tk, nt.newtype, ps.tokens[istart].comments, def.fields, def.field_order, def.field_comments) + store_field_in_record(ps, iv, v.tk, nt.newtype, def, ps.tokens[istart].comments) return i end @@ -4123,8 +4146,7 @@ parse_record_body = function(ps: ParseState, i: integer, def: RecordLikeType): i def.meta_fields = {} def.meta_field_order = {} - def.meta_field_comments = {} - store_field_in_record(ps, i, "__is", typ, {}, def.meta_fields, def.meta_field_order, def.meta_field_comments) + store_field_in_record(ps, i, "__is", typ, def, nil, "meta") end while not (ps.tokens[i].kind == "$EOF$" or ps.tokens[i].tk == "end") do @@ -4164,7 +4186,7 @@ parse_record_body = function(ps: ParseState, i: integer, def: RecordLikeType): i ntt.is_nested_alias = true end - store_field_in_record(ps, iv, v.tk, nt.newtype, comments, def.fields, def.field_order, def.field_comments) + store_field_in_record(ps, iv, v.tk, nt.newtype, def, comments) elseif parse_type_body_fns[tn] and ps.tokens[i+1].tk ~= ":" then if def.typename == "interface" and tn == "record" then i = failskip(ps, i, "interfaces cannot contain record definitions", skip_type_body) @@ -4206,18 +4228,11 @@ parse_record_body = function(ps: ParseState, i: integer, def: RecordLikeType): i end local field_name = v.conststr or v.tk - local fields = def.fields - local field_order = def.field_order - local field_comments = def.field_comments if is_metamethod then if not def.meta_fields then def.meta_fields = {} def.meta_field_order = {} - def.meta_field_comments = {} end - fields = def.meta_fields - field_order = def.meta_field_order - field_comments = def.meta_field_comments if not metamethod_names[field_name] then fail(ps, i - 1, "not a valid metamethod: " .. field_name) end @@ -4233,7 +4248,7 @@ parse_record_body = function(ps: ParseState, i: integer, def: RecordLikeType): i end end - store_field_in_record(ps, iv, field_name, t, comments, fields, field_order, field_comments) + store_field_in_record(ps, iv, field_name, t, def, comments, is_metamethod and "meta" or nil) elseif ps.tokens[i].tk == "=" then local next_word = ps.tokens[i + 1].tk if next_word == "record" or next_word == "enum" then @@ -4714,10 +4729,6 @@ local record Visitor allow_missing_cbs: boolean end -local enum MetaMode - "meta" -end - local function fields_of(t: RecordLikeType, meta?: MetaMode): (function(): string, Type) local i = 1 local field_order, fields: {string}, {string:Type} @@ -8170,6 +8181,7 @@ do copy.fields = {} copy.field_order = {} + copy.field_comments = t.field_comments for i, k in ipairs(t.field_order) do copy.field_order[i] = k copy.fields[k], same = resolve(t.fields[k], same) @@ -8178,6 +8190,7 @@ do if t.meta_fields then copy.meta_fields = {} copy.meta_field_order = {} + copy.meta_field_comments = t.meta_field_comments for i, k in ipairs(t.meta_field_order) do copy.meta_field_order[i] = k copy.meta_fields[k], same = resolve(t.meta_fields[k], same) From b2891e1b7113e8f941a6024ac5c05fe3bc4fe149 Mon Sep 17 00:00:00 2001 From: uped Date: Mon, 9 Jun 2025 21:29:52 +0200 Subject: [PATCH 3/4] fix more issues from code review --- spec/lang/parser/store_comments_spec.lua | 4 ++-- tl.lua | 16 ++++++++++++---- tl.tl | 16 ++++++++++++---- 3 files changed, 26 insertions(+), 10 deletions(-) diff --git a/spec/lang/parser/store_comments_spec.lua b/spec/lang/parser/store_comments_spec.lua index 6627668a..9b44e434 100644 --- a/spec/lang/parser/store_comments_spec.lua +++ b/spec/lang/parser/store_comments_spec.lua @@ -274,7 +274,7 @@ describe("store comments in syntax tree", function() ["__call"] = "-- this is a comment", ["__add"] = "-- another comment", } - for field_name, _ in pairs(record_def.fields) do + for field_name, _ in pairs(record_def.meta_fields) do assert.same(expected_comments[field_name], record_def.meta_field_comments[field_name][1][1].text) end end) @@ -409,7 +409,7 @@ describe("store comments in syntax tree", function() ["__call"] = "-- this is a comment", ["__add"] = "-- another comment", } - for field_name, _ in pairs(interface_def.fields) do + for field_name, _ in pairs(interface_def.meta_fields) do assert.same(expected_comments[field_name], interface_def.meta_field_comments[field_name][1][1].text) end end) diff --git a/tl.lua b/tl.lua index 63eb8574..072d7e17 100644 --- a/tl.lua +++ b/tl.lua @@ -3871,6 +3871,11 @@ do if comments and not field_comments then field_comments = {} + if meta then + def.meta_field_comments = field_comments + else + def.field_comments = field_comments + end end if not fields[field_name] then @@ -3895,7 +3900,7 @@ do end end table.insert(field_comments[field_name], comments) - elseif field_comments[field_name] then + elseif field_comments and field_comments[field_name] then table.insert(field_comments[field_name], {}) end end @@ -3958,14 +3963,18 @@ do parse_enum_body = function(ps, i, def) def.enumset = {} - def.value_comments = {} while ps.tokens[i].tk ~= "$EOF$" and ps.tokens[i].tk ~= "end" do local item i, item = verify_kind(ps, i, "string", "string") if item then local name = unquote(item.tk) def.enumset[name] = true - def.value_comments[name] = item.comments + if item.comments then + if not def.value_comments then + def.value_comments = {} + end + def.value_comments[name] = item.comments + end end end return i, true @@ -4094,7 +4103,6 @@ do parse_record_body = function(ps, i, def) def.fields = {} def.field_order = {} - def.field_comments = {} if ps.tokens[i].tk == "{" then local atype diff --git a/tl.tl b/tl.tl index e6bdfae7..7962d1f5 100644 --- a/tl.tl +++ b/tl.tl @@ -3871,6 +3871,11 @@ local function store_field_in_record(ps: ParseState, i: integer, field_name: str if comments and not field_comments then field_comments = {} + if meta then + def.meta_field_comments = field_comments + else + def.field_comments = field_comments + end end if not fields[field_name] then @@ -3895,7 +3900,7 @@ local function store_field_in_record(ps: ParseState, i: integer, field_name: str end end table.insert(field_comments[field_name], comments) - elseif field_comments[field_name] then + elseif field_comments and field_comments[field_name] then table.insert(field_comments[field_name], {}) end end @@ -3958,14 +3963,18 @@ end parse_enum_body = function(ps: ParseState, i: integer, def: EnumType): integer, boolean def.enumset = {} - def.value_comments = {} while ps.tokens[i].tk ~= "$EOF$" and ps.tokens[i].tk ~= "end" do local item: Node i, item = verify_kind(ps, i, "string", "string") if item then local name = unquote(item.tk) def.enumset[name] = true - def.value_comments[name] = item.comments + if item.comments then + if not def.value_comments then + def.value_comments = {} + end + def.value_comments[name] = item.comments + end end end return i, true @@ -4094,7 +4103,6 @@ end parse_record_body = function(ps: ParseState, i: integer, def: RecordLikeType): integer, boolean def.fields = {} def.field_order = {} - def.field_comments = {} if ps.tokens[i].tk == "{" then local atype: Type From d2d20b26d4dd0d2a211787fca99a26f216689b51 Mon Sep 17 00:00:00 2001 From: uped Date: Mon, 9 Jun 2025 22:04:01 +0200 Subject: [PATCH 4/4] make tests code less confusing --- spec/lang/parser/store_comments_spec.lua | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/spec/lang/parser/store_comments_spec.lua b/spec/lang/parser/store_comments_spec.lua index 9b44e434..8afef8a5 100644 --- a/spec/lang/parser/store_comments_spec.lua +++ b/spec/lang/parser/store_comments_spec.lua @@ -298,7 +298,9 @@ describe("store comments in syntax tree", function() ["bar"] = {"-- this is a comment", "-- another comment"} } for field_name, _ in pairs(record_def.fields) do - for i = 1, #record_def.field_comments[field_name] do + local n = #record_def.field_comments[field_name] + assert.same(2, n) + for i = 1, n do if not expected_comments[field_name][i] then assert.same({}, record_def.field_comments[field_name][i]) else @@ -433,7 +435,9 @@ describe("store comments in syntax tree", function() ["bar"] = {"-- this is a comment", "-- another comment"} } for field_name, _ in pairs(interface_def.fields) do - for i = 1, #interface_def.field_comments[field_name] do + local n = #interface_def.field_comments[field_name] + assert.same(2, n) + for i = 1, n do if not expected_comments[field_name][i] then assert.same({}, interface_def.field_comments[field_name][i]) else @@ -486,7 +490,9 @@ describe("store comments in syntax tree", function() ["f"] = {nil, "--- it can be a boolean too", nil} } for field_name, _ in pairs(record_def.fields) do - for i = 1, #record_def.field_comments[field_name] do + local n = #record_def.field_comments[field_name] + assert.same(3, n) + for i = 1, n do if not expected_comments[field_name][i] then assert.same({}, record_def.field_comments[field_name][i]) else