Skip to content

Devsper Language Support#3401

Closed
rithulkamesh wants to merge 10 commits intoLuaLS:masterfrom
devsper-com:feat/devsper-language-support
Closed

Devsper Language Support#3401
rithulkamesh wants to merge 10 commits intoLuaLS:masterfrom
devsper-com:feat/devsper-language-support

Conversation

@rithulkamesh
Copy link
Copy Markdown

No description provided.

rithulkamesh and others added 8 commits April 21, 2026 10:22
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…n types

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds script/core/diagnostics/devsper-schema.lua which fires warnings on
.devsper files for missing required spec fields (prompt, source, run,
description) and unknown builtin: plugin names. Registered as the
'devsper-schema' diagnostic in the 'devsper' group via proto/diagnostic.lua.
…yield

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces language support for the Devsper workflow engine, including VS Code extension assets, Lua meta-definitions, and custom diagnostics for the Lua Language Server. The review identified several issues in the diagnostic implementation: a correctness bug in devsper-depends.lua where AST nodes were incorrectly accessed, performance inefficiencies due to redundant sorting and multiple AST traversals, and brittle logic in isLocalMethod that relies on specific variable naming rather than the VM's type system.

Comment on lines +51 to +87
-- Pass 2: check depends_on entries in each wf.task spec
guide.eachSourceType(state.ast, 'call', function (source)
await.delay()
if not isLocalMethod(source, 'wf', 'task') then return end
local args = source.args
if not args then return end
local spec = args[2]
if not spec or spec.type ~= 'table' then return end

local dependsOn = tableGet(spec, 'depends_on')
if not dependsOn or dependsOn.type ~= 'table' then return end

-- Build sorted list of declared IDs for error messages
local sortedIds = {}
for k in pairs(declaredIds) do
sortedIds[#sortedIds + 1] = k
end
table.sort(sortedIds)

-- Iterate direct children of the depends_on array table
for i = 1, #dependsOn do
local child = dependsOn[i]
if child and child.type == 'string' then
local val = child[1]
if val and val ~= '*' and not declaredIds[val] then
callback {
start = child.start,
finish = child.finish,
message = ("devsper: unknown task id '%s' in depends_on — declared ids: %s"):format(
val, table.concat(sortedIds, ', ')
),
}
end
end
-- Non-string children (variables, function calls) are silently skipped
end
end)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

This section contains two significant issues:

  1. Correctness Bug: The current implementation iterates over the depends_on table and expects string nodes as direct children. However, in the Lua Language Server AST, array elements in a table are wrapped in tableexp nodes. Consequently, child.type == 'string' will always be false, and the diagnostic will never trigger for unknown task IDs.
  2. Performance Issue: The sortedIds list is being rebuilt and sorted inside the eachSourceType callback for every function call found in the AST. This is inefficient and should be computed once before starting the second pass.
    -- Pass 2: check depends_on entries in each wf.task spec
    local sortedIds = {}
    for k in pairs(declaredIds) do
        sortedIds[#sortedIds + 1] = k
    end
    table.sort(sortedIds)
    local declaredIdsStr = table.concat(sortedIds, ', ')

    guide.eachSourceType(state.ast, 'call', function (source)
        await.delay()
        if not isLocalMethod(source, 'wf', 'task') then return end
        local args = source.args
        if not args then return end
        local spec = args[2]
        if not spec or spec.type ~= 'table' then return end

        local dependsOn = tableGet(spec, 'depends_on')
        if not dependsOn or dependsOn.type ~= 'table' then return end

        -- Iterate direct children of the depends_on array table
        for i = 1, #dependsOn do
            local exp = dependsOn[i]
            if exp and exp.type == 'tableexp' then
                local child = exp.value
                if child and child.type == 'string' then
                    local val = child[1]
                    if val and val ~= '*' and not declaredIds[val] then
                        callback {
                            start   = child.start,
                            finish  = child.finish,
                            message = ("devsper: unknown task id '%s' in depends_on — declared ids: %s"):format(
                                val, declaredIdsStr
                            ),
                        }
                    end
                end
            end
            -- Non-string children (variables, function calls) are silently skipped
        end
    end)

Comment on lines +38 to +49
guide.eachSourceType(state.ast, 'call', function (source)
await.delay()
if not isLocalMethod(source, 'wf', 'task') then return end
local args = source.args
if not args then return end
local idNode = args[1]
if not idNode or idNode.type ~= 'string' then return end
local id = idNode[1]
if id then
declaredIds[id] = { start = idNode.start, finish = idNode.finish }
end
end)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The diagnostic performs two full traversals of all function calls in the AST using guide.eachSourceType. This is inefficient. You can optimize this by performing a single traversal to collect both the declared IDs and the dependency lists that need validation, then performing the validation in a separate loop over the collected data.

Comment on lines +18 to +26
local function isLocalMethod(source, receiverName, methodName)
if not source or source.type ~= 'call' then return false end
local callee = source.node
if not callee or callee.type ~= 'getfield' then return false end
if not callee.field or callee.field[1] ~= methodName then return false end
local receiver = callee.node
if not receiver or receiver.type ~= 'getlocal' then return false end
return receiver[1] == receiverName
end
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The isLocalMethod helper strictly checks for a local variable named wf. This makes the diagnostics brittle, as they will fail to provide feedback if a user renames the variable returned by devsper.workflow() (e.g., local my_wf = devsper.workflow(...)). Consider using the VM's type system to track the object returned by the workflow builder call more robustly.

@rithulkamesh
Copy link
Copy Markdown
Author

Sorry merged to the wrong repo...

@rithulkamesh rithulkamesh deleted the feat/devsper-language-support branch April 21, 2026 05:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant