@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 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
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
|
||||||
|
|
||||||
// Note: We don't support @named syntax in TypeScript functions
|
// Check if this is a named parameter (atXxx pattern)
|
||||||
// Users would need to manually handle named args via an options object
|
if (/^at[A-Z]/.test(paramName)) {
|
||||||
|
named = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return { params, defaults, variadic, named }
|
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 { 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)
|
||||||
|
|
|
||||||
|
|
@ -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'))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user