@named in native functions
This commit is contained in:
parent
1cf14636ff
commit
fa55eb7170
34
GUIDE.md
34
GUIDE.md
|
|
@ -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 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
|
||||
- RETURN with empty stack returns null
|
||||
- HALT with empty stack returns null
|
||||
|
|
|
|||
|
|
@ -71,6 +71,11 @@ export function extractParamInfo(fn: Function): ParamInfo {
|
|||
|
||||
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 {
|
||||
if (defaultStr === 'null') {
|
||||
|
|
@ -84,18 +89,21 @@ export function extractParamInfo(fn: Function): ParamInfo {
|
|||
} else if (/^['"].*['"]$/.test(defaultStr)) {
|
||||
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 {
|
||||
// If parsing fails, ignore the default
|
||||
}
|
||||
} else {
|
||||
// Regular parameter
|
||||
params.push(part)
|
||||
}
|
||||
}
|
||||
const paramName = part.trim()
|
||||
params.push(paramName)
|
||||
|
||||
// Note: We don't support @named syntax in TypeScript functions
|
||||
// Users would need to manually handle named args via an options object
|
||||
// Check if this is a named parameter (atXxx pattern)
|
||||
if (/^at[A-Z]/.test(paramName)) {
|
||||
named = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { params, defaults, variadic, named }
|
||||
}
|
||||
|
|
|
|||
19
src/vm.ts
19
src/vm.ts
|
|
@ -3,7 +3,7 @@ import type { ExceptionHandler } from "./exception"
|
|||
import { type Frame } from "./frame"
|
||||
import { OpCode } from "./opcode"
|
||||
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"
|
||||
|
||||
export class VM {
|
||||
|
|
@ -442,9 +442,10 @@ export class VM {
|
|||
// Bind parameters using the same priority as Reef functions
|
||||
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
|
||||
if (paramInfo.variadic) nativeFixedParamCount--
|
||||
if (paramInfo.named) nativeFixedParamCount--
|
||||
|
||||
// Track which positional args have been consumed
|
||||
let nativePositionalArgIndex = 0
|
||||
|
|
@ -456,7 +457,7 @@ export class VM {
|
|||
// Check if named argument was provided for this param
|
||||
if (namedArgs.has(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) {
|
||||
nativeArgs.push(positionalArgs[nativePositionalArgIndex]!)
|
||||
nativePositionalArgIndex++
|
||||
|
|
@ -475,7 +476,17 @@ export class VM {
|
|||
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
|
||||
const result = await fn.fn(...nativeArgs)
|
||||
|
|
|
|||
|
|
@ -507,3 +507,117 @@ test("Named arguments - works with both wrapped and non-wrapped functions", asyn
|
|||
const result = await vm.run()
|
||||
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'))
|
||||
}
|
||||
})
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user