Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 7 additions & 6 deletions runtime/lua/code/manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -438,12 +438,13 @@ func (cm *Manager) Compile(
func (cm *Manager) AddNode(_ context.Context, node Node, deps []Import) error {
// Spawn pointer from value
nodePtr := &Node{
ID: node.ID,
Kind: node.Kind,
Source: node.Source,
Method: node.Method,
Module: node.Module,
Version: cm.nextVersion(HashNode(&node)),
ID: node.ID,
Kind: node.Kind,
Source: node.Source,
Method: node.Method,
Module: node.Module,
Manifest: node.Manifest,
Version: cm.nextVersion(HashNode(&node)),
}

// Eager compilation check: validate source code before adding to graph
Expand Down
39 changes: 39 additions & 0 deletions runtime/lua/code/manager_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
glua "github.com/wippyai/go-lua"
"github.com/wippyai/go-lua/types/io"
"github.com/wippyai/go-lua/types/typ"
ctxapi "github.com/wippyai/runtime/api/context"
"github.com/wippyai/runtime/api/event"
"github.com/wippyai/runtime/api/registry"
Expand Down Expand Up @@ -429,6 +431,43 @@ func TestManager_AddNode(t *testing.T) {
}
}

func TestManager_AddNodePreservesManifest(t *testing.T) {
cm, err := NewCodeManager(zap.NewNop(), &testEventBus{}, Config{})
require.NoError(t, err)

manifest := io.NewManifest("registry")
manifest.SetExport(typ.NewRecord().
Field("get", typ.Func().Returns(typ.NewRecord().Field("id", typ.String).Build()).Build()).
Build())
module := &api.ModuleDef{
Name: "registry",
Types: func() *io.Manifest { return manifest },
}
nodeID := registry.NewID("", module.Name)
require.NoError(t, cm.AddNode(context.Background(), Node{
ID: nodeID,
Kind: api.ModuleKind,
Module: module,
Manifest: manifest,
}, nil))

node, err := cm.memGraph.GetNode(nodeID)
require.NoError(t, err)
require.Same(t, manifest, node.Manifest)

consumerID := registry.NewID("app", "consumer")
require.NoError(t, cm.AddNode(context.Background(), Node{
ID: consumerID,
Kind: api.Function,
}, []Import{{ID: nodeID, Alias: "registry"}}))
require.Same(t, manifest, cm.GetNodeDependencyManifests(consumerID)["registry"],
"dependency aliases must retain the supplied host-module manifest")

cm.AddBuiltinType(module)
require.NotEmpty(t, cm.BuiltinManifestHash(),
"host-module manifests must participate in the builtin cache fingerprint")
}

func TestManager_UpdateNode(t *testing.T) {
logger := zap.NewNop()
bus := &testEventBus{}
Expand Down
29 changes: 28 additions & 1 deletion runtime/lua/modules/fs/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,30 @@ var fileInfoType = typ.NewRecord().
Field("type", typ.String).
Build()

// dirEntryType is the value produced by FS:readdir's generic-for iterator.
// It intentionally differs from FileInfo: readdir exposes only the entry name
// and kind, matching dirIteratorNext in fs.go.
var dirEntryType = typ.NewRecord().
Field("name", typ.String).
Field("type", typ.NewUnion(
typ.LiteralString(typeFile),
typ.LiteralString(typeDir),
)).
Build()

// Lua's generic-for protocol calls the generator as generator(state, control).
// dirIteratorNext consumes the state userdata and ignores the control value.
var dirIteratorType = typ.Func().
Param("state", typ.Any).
OptParam("control", typ.Any).
Returns(typ.NewOptional(dirEntryType)).
Build()

// The iterator state is opaque to Lua callers. A named, methodless interface
// is nominal in go-lua and therefore models the userdata without inventing
// operations that the runtime does not provide.
var dirIteratorStateType = typ.NewInterface("fs.DirIterator", nil)

var scannerType = typ.NewInterface("fs.Scanner", []typ.Method{
{Name: "scan", Type: typ.Func().
Param("self", typ.Self).
Expand Down Expand Up @@ -108,7 +132,10 @@ func init() {
{Name: "readdir", Type: typ.Func().
Param("self", typ.Self).
Param("path", typ.String).
Returns(typ.NewArray(fileInfoType), typ.NewOptional(typ.LuaError)).
Returns(
typ.NewOptional(dirIteratorType),
typ.NewUnion(dirIteratorStateType, typ.LuaError),
).
Build()},
{Name: "mkdir", Type: typ.Func().
Param("self", typ.Self).
Expand Down
90 changes: 90 additions & 0 deletions runtime/lua/modules/fs/types_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
// SPDX-License-Identifier: MPL-2.0

package fs

import (
"testing"

"github.com/stretchr/testify/require"
"github.com/wippyai/go-lua/types/io"
"github.com/wippyai/go-lua/types/typ"
"github.com/wippyai/runtime/runtime/lua/code"
)

func TestReaddirTypeReturnsDirectoryEntryIterator(t *testing.T) {
manifest := ModuleTypes()
fs, ok := manifest.LookupType("FS")
if !ok {
t.Fatal("FS type is not defined")
}

methods := fs.(*typ.Interface).Methods
var readdir *typ.Function
for _, method := range methods {
if method.Name == "readdir" {
readdir = method.Type
break
}
}
if readdir == nil {
t.Fatal("readdir method is not defined")
}
if len(readdir.Returns) != 2 {
t.Fatalf("readdir returns %d values, want iterator and state", len(readdir.Returns))
}

optionalIterator, ok := readdir.Returns[0].(*typ.Optional)
if !ok {
t.Fatalf("readdir iterator = %T, want optional function", readdir.Returns[0])
}
iterator, ok := optionalIterator.Inner.(*typ.Function)
if !ok {
t.Fatalf("readdir iterator = %T, want function", optionalIterator.Inner)
}
if len(iterator.Params) != 2 || iterator.Params[0].Name != "state" || iterator.Params[0].Optional || !iterator.Params[1].Optional {
t.Fatalf("iterator params = %+v, want required state and optional control", iterator.Params)
}
if len(iterator.Returns) != 1 {
t.Fatalf("iterator returns %d values, want directory entry", len(iterator.Returns))
}
entry, ok := iterator.Returns[0].(*typ.Optional)
if !ok {
t.Fatalf("iterator entry = %T, want optional record", iterator.Returns[0])
}
record, ok := entry.Inner.(*typ.Record)
if !ok {
t.Fatalf("iterator entry = %T, want record", entry.Inner)
}
name := record.GetField("name")
if name == nil || !typ.TypeEquals(name.Type, typ.String) {
t.Fatalf("entry.name = %v, want string", name)
}
kind := record.GetField("type")
wantKind := typ.NewUnion(typ.LiteralString(typeFile), typ.LiteralString(typeDir))
if kind == nil || !typ.TypeEquals(kind.Type, wantKind) {
t.Fatalf("entry.type = %v, want %s", kind, wantKind)
}

wantStateOrError := typ.NewUnion(dirIteratorStateType, typ.LuaError)
if !typ.TypeEquals(readdir.Returns[1], wantStateOrError) {
t.Fatalf("readdir state/error = %s, want %s", readdir.Returns[1], wantStateOrError)
}
}

func TestReaddirTypeChecksGenericForProtocol(t *testing.T) {
tc := code.NewTypeChecker(code.TypeCheckConfig{Enabled: true, Strict: true}, nil)

_, diagnostics, err := tc.Check(`
local fs = require("fs")
local volume = fs.get("app:temp")
local iterator, state = volume:readdir("/")
if iterator then
for entry in iterator, state do
local name: string = entry.name
local kind: "file" | "directory" = entry.type
end
end
`, "fs_readdir_types.lua", map[string]*io.Manifest{"fs": ModuleTypes()})
require.NoError(t, err)
require.False(t, code.HasErrors(diagnostics), "unexpected diagnostics: %v", diagnostics)
}
9 changes: 8 additions & 1 deletion runtime/lua/modules/process/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,13 @@ var processOptionsType = typ.NewRecord().
Field("upgradable", typ.Boolean).
Build()

// set_options applies a partial update. get_options always returns both fields,
// but callers may set either option independently (or pass an empty table).
var processOptionsUpdateType = typ.NewRecord().
OptField("trap_links", typ.Boolean).
OptField("upgradable", typ.Boolean).
Build()

var eventType = typ.NewRecord().
Field("CANCEL", typ.String).
Field("EXIT", typ.String).
Expand Down Expand Up @@ -236,7 +243,7 @@ func ModuleTypes() *io.Manifest {
Returns(processOptionsType).
Build()},
{Name: "set_options", Type: typ.Func().
Param("opts", processOptionsType).
Param("opts", processOptionsUpdateType).
Returns(typ.Boolean, typ.NewOptional(typ.LuaError)).
Build()},
{Name: "monitor", Type: typ.Func().
Expand Down
28 changes: 28 additions & 0 deletions runtime/lua/modules/process/types_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// SPDX-License-Identifier: MPL-2.0

package process

import (
"testing"

"github.com/stretchr/testify/require"
"github.com/wippyai/go-lua/types/io"
"github.com/wippyai/runtime/runtime/lua/code"
)

func TestSetOptionsTypeAcceptsPartialUpdates(t *testing.T) {
tc := code.NewTypeChecker(code.TypeCheckConfig{Enabled: true, Strict: true}, nil)

_, diagnostics, err := tc.Check(`
local process = require("process")
process.set_options({ trap_links = true })
process.set_options({ upgradable = true })
process.set_options({})

local options = process.get_options()
local trap_links: boolean = options.trap_links
local upgradable: boolean = options.upgradable
`, "process_options_types.lua", map[string]*io.Manifest{"process": ModuleTypes()})
require.NoError(t, err)
require.False(t, code.HasErrors(diagnostics), "unexpected diagnostics: %v", diagnostics)
}
31 changes: 19 additions & 12 deletions runtime/lua/modules/queue/module.go
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,7 @@ func messageHeader(l *lua.LState) int {
return 2
}

l.Push(toLuaValue(val))
l.Push(normalizeHeaderValue(val))
l.Push(lua.LNil)
return 2
}
Expand All @@ -228,7 +228,7 @@ func messageHeaders(l *lua.LState) int {
headers := msg.delivery.Message.Headers
tbl := lua.CreateTable(0, len(headers))
for key, val := range headers {
tbl.RawSetString(key, toLuaValue(val))
tbl.RawSetString(key, normalizeHeaderValue(val))
}
l.Push(tbl)
l.Push(lua.LNil)
Expand Down Expand Up @@ -353,7 +353,11 @@ func info(l *lua.LState) int {

tbl := lua.CreateTable(0, len(bag))
for k, v := range bag {
tbl.RawSetString(k, toLuaValue(v))
lv, convErr := luaconv.GoToLua(v)
if convErr != nil {
return internalError(l, convErr, "convert queue info")
}
tbl.RawSetString(k, lv)
}
l.Push(tbl)
l.Push(lua.LNil)
Expand All @@ -377,18 +381,21 @@ func toGoValue(v lua.LValue) any {
}
}

func toLuaValue(val any) lua.LValue {
// normalizeHeaderValue gives Lua consumers one stable header contract across
// drivers. The internal attrs.Bag remains typed because drivers need numbers,
// booleans, and binary values while publishing, but consumed metadata is
// exposed as a string just like other header-oriented runtime APIs.
//
// A nil value is treated as absent. Raw bytes become a Lua string without the
// "[1 2 3]" formatting that fmt.Sprint would otherwise produce.
func normalizeHeaderValue(val any) lua.LValue {
switch v := val.(type) {
case string:
return lua.LString(v)
case int:
return lua.LNumber(v)
case int64:
return lua.LNumber(v)
case float64:
return lua.LNumber(v)
case bool:
return lua.LBool(v)
case []byte:
return lua.LString(string(v))
case nil:
return lua.LNil
default:
return lua.LString(fmt.Sprintf("%v", v))
}
Expand Down
Loading