Skip to content

Commit 42535f2

Browse files
authored
Add special {{@now}} parameter that returns current date in ISO format (#40)
- Modified resolveExpression in compile.ts to handle '@now' identifier - Added test case for {{@now}} functionality - {{@now}} is replaced at runtime with new Date().toISOString()
1 parent 929377c commit 42535f2

File tree

11 files changed

+106
-5
lines changed

11 files changed

+106
-5
lines changed

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "promptl-ai",
3-
"version": "0.7.5",
3+
"version": "0.7.6",
44
"author": "Latitude Data",
55
"license": "MIT",
66
"description": "Compiler for PromptL, the prompt language",
@@ -24,6 +24,7 @@
2424
"dist"
2525
],
2626
"scripts": {
27+
"prepare": "npm run build",
2728
"dev": "rollup -c -w",
2829
"build": "rollup -c",
2930
"build:lib": "rollup -c rollup.config.mjs",

src/compiler/compile.test.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,35 @@ describe('variable assignment', async () => {
181181
expect(result).toBe('')
182182
})
183183

184+
it('special $now parameter returns current date in ISO format', async () => {
185+
const prompt = `
186+
{{ $now }}
187+
`
188+
const result = await getCompiledText(prompt)
189+
// Check that it's a valid ISO date string (JSON stringified)
190+
expect(result).toMatch(/^"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z"$/)
191+
})
192+
193+
it('special $now can be used in expressions', async () => {
194+
const prompt = `
195+
{{ time = $now }}
196+
{{ time }}
197+
`
198+
const result = await getCompiledText(prompt)
199+
// Check that it's a valid ISO date string (JSON stringified)
200+
expect(result).toMatch(/^"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z"$/)
201+
})
202+
203+
it('special $now can be used in method calls', async () => {
204+
const prompt = `
205+
{{ $now.getTime() }}
206+
`
207+
const result = await getCompiledText(prompt)
208+
const timestamp = parseInt(result.trim())
209+
expect(timestamp).toBeGreaterThan(0)
210+
expect(timestamp).toBeLessThan(Date.now() + 1000) // within 1 second
211+
})
212+
184213
it('parameters are available as variables in the prompt', async () => {
185214
const prompt = `
186215
{{ foo }}

src/compiler/compile.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import type {
3434
ResolveBaseNodeProps,
3535
} from './types'
3636
import { getCommonIndent, removeCommonIndent } from './utils'
37+
import { SPECIAL_RESOLVERS } from '../constants'
3738

3839
export type CompilationStatus = {
3940
completed: boolean
@@ -60,6 +61,7 @@ export class Compile {
6061
private globalScope: Scope
6162
private defaultRole: MessageRole
6263
private includeSourceMap: boolean
64+
private builtins: Record<string, () => any>
6365

6466
private messages: Message[] = []
6567
private globalConfig: Config | undefined
@@ -97,6 +99,7 @@ export class Compile {
9799
this.ast = ast
98100
this.stepResponse = stepResponse
99101
this.defaultRole = defaultRole
102+
this.builtins = SPECIAL_RESOLVERS
100103
this.referenceFn = referenceFn
101104
this.fullPath = fullPath
102105
this.includeSourceMap = includeSourceMap
@@ -294,6 +297,7 @@ export class Compile {
294297
return await resolveLogicNode({
295298
node: expression,
296299
scope,
300+
builtins: this.builtins,
297301
raiseError: this.expressionError.bind(this),
298302
})
299303
}

src/compiler/logic/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ export async function updateScopeContextForNode(
2727
props: UpdateScopeContextProps<Node>,
2828
) {
2929
const type = props.node.type as NodeType
30-
if (!nodeResolvers[type]) {
30+
if (!updateScopeContextResolvers[type]) {
3131
throw new Error(`Unknown node type: ${type}`)
3232
}
3333

src/compiler/logic/nodes/assignmentExpression.ts

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -136,18 +136,27 @@ async function assignToProperty({
136136
export function updateScopeContext({
137137
node,
138138
scopeContext,
139+
builtins,
139140
raiseError,
140141
}: UpdateScopeContextProps<AssignmentExpression>) {
141142
const assignmentOperator = node.operator
142143
if (!(assignmentOperator in ASSIGNMENT_OPERATOR_METHODS)) {
143144
raiseError(errors.unsupportedOperator(assignmentOperator), node)
144145
}
145146

146-
updateScopeContextForNode({ node: node.right, scopeContext, raiseError })
147+
updateScopeContextForNode({
148+
node: node.right,
149+
scopeContext,
150+
builtins,
151+
raiseError,
152+
})
147153

148154
if (node.left.type === 'Identifier') {
149155
// Variable assignment
150156
const assignedVariableName = (node.left as Identifier).name
157+
if (assignedVariableName in builtins) {
158+
raiseError(errors.assignmentToBuiltin(assignedVariableName), node)
159+
}
151160
if (assignmentOperator != '=') {
152161
// Update an existing variable
153162
if (!scopeContext.definedVariables.has(assignedVariableName)) {
@@ -159,7 +168,12 @@ export function updateScopeContext({
159168
}
160169

161170
if (node.left.type === 'MemberExpression') {
162-
updateScopeContextForNode({ node: node.left, scopeContext, raiseError })
171+
updateScopeContextForNode({
172+
node: node.left,
173+
scopeContext,
174+
builtins,
175+
raiseError,
176+
})
163177
return
164178
}
165179

src/compiler/logic/nodes/identifier.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,14 @@ import type { Identifier } from 'estree'
99
* ### Identifier
1010
* Represents a variable from the scope.
1111
*/
12-
export async function resolve({ node, scope }: ResolveNodeProps<Identifier>) {
12+
export async function resolve({
13+
node,
14+
scope,
15+
builtins,
16+
}: ResolveNodeProps<Identifier>) {
17+
if (node.name in builtins) {
18+
return builtins[node.name]!()
19+
}
1320
if (!scope.exists(node.name)) {
1421
return undefined
1522
}
@@ -19,8 +26,12 @@ export async function resolve({ node, scope }: ResolveNodeProps<Identifier>) {
1926
export function updateScopeContext({
2027
node,
2128
scopeContext,
29+
builtins,
2230
raiseError,
2331
}: UpdateScopeContextProps<Identifier>) {
32+
if (node.name in builtins) {
33+
return
34+
}
2435
if (!scopeContext.definedVariables.has(node.name)) {
2536
if (scopeContext.onlyPredefinedVariables === undefined) {
2637
scopeContext.usedUndefinedVariables.add(node.name)

src/compiler/logic/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,13 @@ type RaiseErrorFn<T = void | never> = (
2626
export type ResolveNodeProps<N extends Node> = {
2727
node: N
2828
scope: Scope
29+
builtins: Record<string, () => any>
2930
raiseError: RaiseErrorFn<never>
3031
}
3132

3233
export type UpdateScopeContextProps<N extends Node> = {
3334
node: N
3435
scopeContext: ScopeContext
36+
builtins: Record<string, () => any>
3537
raiseError: RaiseErrorFn<void>
3638
}

src/compiler/scan.test.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -588,6 +588,33 @@ describe('parameters', async () => {
588588

589589
expect(metadata.parameters).toEqual(new Set(['foo', 'bar', 'arr']))
590590
})
591+
592+
it('does not include special identifiers as parameters', async () => {
593+
const prompt = `
594+
{{ $now }}
595+
`
596+
597+
const metadata = await scan({
598+
prompt: removeCommonIndent(prompt),
599+
})
600+
601+
expect(metadata.parameters).toEqual(new Set())
602+
})
603+
604+
it('raises error when assigning to builtin', async () => {
605+
const prompt = `
606+
{{ $now = "2023-01-01" }}
607+
`
608+
609+
const metadata = await scan({
610+
prompt: removeCommonIndent(prompt),
611+
})
612+
613+
expect(metadata.errors).toHaveLength(1)
614+
expect(metadata.errors[0]!.message).toBe(
615+
"Cannot assign to builtin variable: '$now'",
616+
)
617+
})
591618
})
592619

593620
describe('referenced prompts', async () => {

src/compiler/scan.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
CUSTOM_MESSAGE_ROLE_ATTR,
55
REFERENCE_DEPTH_LIMIT,
66
REFERENCE_PATH_ATTR,
7+
SPECIAL_RESOLVERS,
78
TAG_NAMES,
89
} from '$promptl/constants'
910
import CompileError, { error } from '$promptl/error/error'
@@ -57,6 +58,7 @@ export class Scan {
5758
private withParameters?: string[]
5859
private requireConfig: boolean
5960
private configSchema?: z.ZodType
61+
private builtins: Record<string, () => any>
6062

6163
private config?: Config
6264
private configPosition?: { start: number; end: number }
@@ -95,6 +97,7 @@ export class Scan {
9597
this.withParameters = withParameters
9698
this.configSchema = configSchema
9799
this.requireConfig = requireConfig ?? false
100+
this.builtins = SPECIAL_RESOLVERS
98101

99102
this.resolvedPrompt = document.content
100103
this.includedPromptPaths = new Set([this.fullPath])
@@ -194,6 +197,7 @@ export class Scan {
194197
await updateScopeContextForNode({
195198
node,
196199
scopeContext,
200+
builtins: this.builtins,
197201
raiseError: this.expressionError.bind(this),
198202
})
199203
}

src/constants.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,3 +40,8 @@ export enum KEYWORDS {
4040

4141
export const RESERVED_KEYWORDS = Object.values(KEYWORDS)
4242
export const RESERVED_TAGS = Object.values(TAG_NAMES)
43+
export const SPECIAL_IDENTIFIERS = new Set(['$now'])
44+
45+
export const SPECIAL_RESOLVERS: Record<string, () => unknown> = {
46+
$now: () => new Date(),
47+
}

0 commit comments

Comments
 (0)