@named in native functions

This commit is contained in:
Chris Wanstrath 2025-10-17 14:05:12 -07:00
parent 1cf14636ff
commit fa55eb7170
4 changed files with 176 additions and 9 deletions

View File

@ -642,6 +642,40 @@ CALL ; → "Hi, Alice!"
**Named Arguments**: Native functions support named arguments. Parameter names are extracted from the function signature at call time, and arguments are bound using the same priority as Reef functions (named arg > positional arg > default > null). **Named Arguments**: Native functions support named arguments. Parameter names are extracted from the function signature at call time, and arguments are bound using the same priority as Reef functions (named arg > positional arg > default > null).
**@named Pattern**: Parameters starting with `at` followed by an uppercase letter (e.g., `atOptions`, `atNamed`) collect unmatched named arguments:
```typescript
// Basic @named - collects all named args
vm.registerFunction('greet', (atNamed: any = {}) => {
return `Hello, ${atNamed.name || 'World'}!`
})
// Mixed positional and @named
vm.registerFunction('configure', (name: string, atOptions: any = {}) => {
return {
name,
debug: atOptions.debug || false,
port: atOptions.port || 3000
}
})
```
Bytecode example:
```
; Call with mixed positional and named args
LOAD configure
PUSH "myApp" ; positional arg → name
PUSH "debug"
PUSH true
PUSH "port"
PUSH 8080
PUSH 1 ; 1 positional arg
PUSH 2 ; 2 named args (debug, port)
CALL ; atOptions receives {debug: true, port: 8080}
```
Named arguments that match fixed parameter names are bound to those parameters. Remaining unmatched named arguments are collected into the `atXxx` parameter as a plain JavaScript object.
### Empty Stack ### Empty Stack
- RETURN with empty stack returns null - RETURN with empty stack returns null
- HALT with empty stack returns null - HALT with empty stack returns null

View File

@ -71,6 +71,11 @@ export function extractParamInfo(fn: Function): ParamInfo {
params.push(paramName) params.push(paramName)
// Check if this is a named parameter (atXxx pattern)
if (/^at[A-Z]/.test(paramName)) {
named = true
}
// Try to parse the default value (only simple literals) // Try to parse the default value (only simple literals)
try { try {
if (defaultStr === 'null') { if (defaultStr === 'null') {
@ -84,18 +89,21 @@ export function extractParamInfo(fn: Function): ParamInfo {
} else if (/^['"].*['"]$/.test(defaultStr)) { } else if (/^['"].*['"]$/.test(defaultStr)) {
defaults[paramName] = toValue(defaultStr.slice(1, -1)) defaults[paramName] = toValue(defaultStr.slice(1, -1))
} }
// For complex defaults, we skip them and let the function's own default be used // For complex defaults (like {}), we skip them and let the function's own default be used
} catch { } catch {
// If parsing fails, ignore the default // If parsing fails, ignore the default
} }
} else { } else {
// Regular parameter // Regular parameter
params.push(part) const paramName = part.trim()
params.push(paramName)
// Check if this is a named parameter (atXxx pattern)
if (/^at[A-Z]/.test(paramName)) {
named = true
}
} }
} }
// Note: We don't support @named syntax in TypeScript functions
// Users would need to manually handle named args via an options object
return { params, defaults, variadic, named } return { params, defaults, variadic, named }
} }

View File

@ -3,7 +3,7 @@ import type { ExceptionHandler } from "./exception"
import { type Frame } from "./frame" import { type Frame } from "./frame"
import { OpCode } from "./opcode" import { OpCode } from "./opcode"
import { Scope } from "./scope" import { Scope } from "./scope"
import { type Value, type NativeFunction, toValue, toNumber, isTrue, isEqual, toString } from "./value" import { type Value, type NativeFunction, toValue, toNumber, isTrue, isEqual, toString, fromValue } from "./value"
import { extractParamInfo, wrapNative, isWrapped, getOriginalFunction } from "./function" import { extractParamInfo, wrapNative, isWrapped, getOriginalFunction } from "./function"
export class VM { export class VM {
@ -442,9 +442,10 @@ export class VM {
// Bind parameters using the same priority as Reef functions // Bind parameters using the same priority as Reef functions
const nativeArgs: Value[] = [] const nativeArgs: Value[] = []
// Determine how many params are fixed (excluding variadic) // Determine how many params are fixed (excluding variadic and named)
let nativeFixedParamCount = paramInfo.params.length let nativeFixedParamCount = paramInfo.params.length
if (paramInfo.variadic) nativeFixedParamCount-- if (paramInfo.variadic) nativeFixedParamCount--
if (paramInfo.named) nativeFixedParamCount--
// Track which positional args have been consumed // Track which positional args have been consumed
let nativePositionalArgIndex = 0 let nativePositionalArgIndex = 0
@ -456,7 +457,7 @@ export class VM {
// Check if named argument was provided for this param // Check if named argument was provided for this param
if (namedArgs.has(paramName)) { if (namedArgs.has(paramName)) {
nativeArgs.push(namedArgs.get(paramName)!) nativeArgs.push(namedArgs.get(paramName)!)
namedArgs.delete(paramName) // Remove so it doesn't cause issues namedArgs.delete(paramName) // Remove from named args so it won't go to @named
} else if (nativePositionalArgIndex < positionalArgs.length) { } else if (nativePositionalArgIndex < positionalArgs.length) {
nativeArgs.push(positionalArgs[nativePositionalArgIndex]!) nativeArgs.push(positionalArgs[nativePositionalArgIndex]!)
nativePositionalArgIndex++ nativePositionalArgIndex++
@ -475,7 +476,17 @@ export class VM {
nativeArgs.push(...remainingArgs) nativeArgs.push(...remainingArgs)
} }
// Native functions don't support @named parameter - extra named args are ignored // Handle named parameter (collect remaining unmatched named args)
// Parameter names matching atXxx pattern (e.g., atOptions, atNamed) collect extra named args
if (paramInfo.named) {
const namedDict = new Map<string, Value>()
for (const [key, value] of namedArgs) {
namedDict.set(key, value)
}
// Convert dict to plain JavaScript object for the native function
const namedObj = fromValue({ type: 'dict', value: namedDict })
nativeArgs.push(toValue(namedObj))
}
// Call the native function with bound args // Call the native function with bound args
const result = await fn.fn(...nativeArgs) const result = await fn.fn(...nativeArgs)

View File

@ -507,3 +507,117 @@ test("Named arguments - works with both wrapped and non-wrapped functions", asyn
const result = await vm.run() const result = await vm.run()
expect(result).toEqual({ type: 'number', value: 25 }) // 15 + 10 expect(result).toEqual({ type: 'number', value: 25 }) // 15 + 10
}) })
test("@named pattern - basic atNamed parameter", async () => {
const bytecode = toBytecode(`
LOAD greet
PUSH "name"
PUSH "Alice"
PUSH "greeting"
PUSH "Hi"
PUSH "extra"
PUSH "value"
PUSH 0
PUSH 3
CALL
`)
const vm = new VM(bytecode)
vm.registerFunction('greet', (atNamed: any = {}) => {
const name = atNamed.name || 'Unknown'
const greeting = atNamed.greeting || 'Hello'
const extra = atNamed.extra || ''
return `${greeting}, ${name}! Extra: ${extra}`
})
const result = await vm.run()
expect(result).toEqual({ type: 'string', value: 'Hi, Alice! Extra: value' })
})
test("@named pattern - mixed positional and atOptions", async () => {
const bytecode = toBytecode(`
LOAD configure
PUSH "app"
PUSH "debug"
PUSH true
PUSH "port"
PUSH 8080
PUSH 1
PUSH 2
CALL
`)
const vm = new VM(bytecode)
vm.registerFunction('configure', (name: string, atOptions: any = {}) => {
return {
name,
debug: atOptions.debug || false,
port: atOptions.port || 3000
}
})
const result = await vm.run()
expect(result.type).toBe('dict')
if (result.type === 'dict') {
expect(result.value.get('name')).toEqual(toValue('app'))
expect(result.value.get('debug')).toEqual(toValue(true))
expect(result.value.get('port')).toEqual(toValue(8080))
}
})
test("@named pattern - with default empty object", async () => {
const bytecode = toBytecode(`
LOAD build
PUSH "project"
PUSH 1
PUSH 0
CALL
`)
const vm = new VM(bytecode)
vm.registerFunction('build', (name: string, atConfig: any = {}) => {
return `Building ${name} with ${Object.keys(atConfig).length} options`
})
const result = await vm.run()
expect(result).toEqual({ type: 'string', value: 'Building project with 0 options' })
})
test("@named pattern - collects only unmatched named args", async () => {
const bytecode = toBytecode(`
LOAD process
PUSH "file"
PUSH "test.txt"
PUSH "mode"
PUSH "read"
PUSH "extra1"
PUSH "value1"
PUSH "extra2"
PUSH "value2"
PUSH 0
PUSH 4
CALL
`)
const vm = new VM(bytecode)
vm.registerFunction('process', (file: string, mode: string, atOptions: any = {}) => {
// file and mode are matched, extra1 and extra2 go to atOptions
return {
file,
mode,
optionCount: Object.keys(atOptions).length,
extra1: atOptions.extra1,
extra2: atOptions.extra2
}
})
const result = await vm.run()
expect(result.type).toBe('dict')
if (result.type === 'dict') {
expect(result.value.get('file')).toEqual(toValue('test.txt'))
expect(result.value.get('mode')).toEqual(toValue('read'))
expect(result.value.get('optionCount')).toEqual(toValue(2))
expect(result.value.get('extra1')).toEqual(toValue('value1'))
expect(result.value.get('extra2')).toEqual(toValue('value2'))
}
})