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
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "promptl-ai",
"version": "0.7.5",
"version": "0.7.6",
"author": "Latitude Data",
"license": "MIT",
"description": "Compiler for PromptL, the prompt language",
Expand All @@ -24,6 +24,7 @@
"dist"
],
"scripts": {
"prepare": "npm run build",
"dev": "rollup -c -w",
"build": "rollup -c",
"build:lib": "rollup -c rollup.config.mjs",
Expand Down
29 changes: 29 additions & 0 deletions src/compiler/compile.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,35 @@ describe('variable assignment', async () => {
expect(result).toBe('')
})

it('special $now parameter returns current date in ISO format', async () => {
const prompt = `
{{ $now }}
`
const result = await getCompiledText(prompt)
// Check that it's a valid ISO date string (JSON stringified)
expect(result).toMatch(/^"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z"$/)
})

it('special $now can be used in expressions', async () => {
const prompt = `
{{ time = $now }}
{{ time }}
`
const result = await getCompiledText(prompt)
// Check that it's a valid ISO date string (JSON stringified)
expect(result).toMatch(/^"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z"$/)
})

it('special $now can be used in method calls', async () => {
const prompt = `
{{ $now.getTime() }}
`
const result = await getCompiledText(prompt)
const timestamp = parseInt(result.trim())
expect(timestamp).toBeGreaterThan(0)
expect(timestamp).toBeLessThan(Date.now() + 1000) // within 1 second
})

it('parameters are available as variables in the prompt', async () => {
const prompt = `
{{ foo }}
Expand Down
4 changes: 4 additions & 0 deletions src/compiler/compile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import type {
ResolveBaseNodeProps,
} from './types'
import { getCommonIndent, removeCommonIndent } from './utils'
import { SPECIAL_RESOLVERS } from '../constants'

export type CompilationStatus = {
completed: boolean
Expand All @@ -60,6 +61,7 @@ export class Compile {
private globalScope: Scope
private defaultRole: MessageRole
private includeSourceMap: boolean
private builtins: Record<string, () => any>

private messages: Message[] = []
private globalConfig: Config | undefined
Expand Down Expand Up @@ -97,6 +99,7 @@ export class Compile {
this.ast = ast
this.stepResponse = stepResponse
this.defaultRole = defaultRole
this.builtins = SPECIAL_RESOLVERS
this.referenceFn = referenceFn
this.fullPath = fullPath
this.includeSourceMap = includeSourceMap
Expand Down Expand Up @@ -294,6 +297,7 @@ export class Compile {
return await resolveLogicNode({
node: expression,
scope,
builtins: this.builtins,
raiseError: this.expressionError.bind(this),
})
}
Expand Down
2 changes: 1 addition & 1 deletion src/compiler/logic/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export async function updateScopeContextForNode(
props: UpdateScopeContextProps<Node>,
) {
const type = props.node.type as NodeType
if (!nodeResolvers[type]) {
if (!updateScopeContextResolvers[type]) {
throw new Error(`Unknown node type: ${type}`)
}

Expand Down
18 changes: 16 additions & 2 deletions src/compiler/logic/nodes/assignmentExpression.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,18 +136,27 @@ async function assignToProperty({
export function updateScopeContext({
node,
scopeContext,
builtins,
raiseError,
}: UpdateScopeContextProps<AssignmentExpression>) {
const assignmentOperator = node.operator
if (!(assignmentOperator in ASSIGNMENT_OPERATOR_METHODS)) {
raiseError(errors.unsupportedOperator(assignmentOperator), node)
}

updateScopeContextForNode({ node: node.right, scopeContext, raiseError })
updateScopeContextForNode({
node: node.right,
scopeContext,
builtins,
raiseError,
})

if (node.left.type === 'Identifier') {
// Variable assignment
const assignedVariableName = (node.left as Identifier).name
if (assignedVariableName in builtins) {
raiseError(errors.assignmentToBuiltin(assignedVariableName), node)
}
if (assignmentOperator != '=') {
// Update an existing variable
if (!scopeContext.definedVariables.has(assignedVariableName)) {
Expand All @@ -159,7 +168,12 @@ export function updateScopeContext({
}

if (node.left.type === 'MemberExpression') {
updateScopeContextForNode({ node: node.left, scopeContext, raiseError })
updateScopeContextForNode({
node: node.left,
scopeContext,
builtins,
raiseError,
})
return
}

Expand Down
13 changes: 12 additions & 1 deletion src/compiler/logic/nodes/identifier.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,14 @@ import type { Identifier } from 'estree'
* ### Identifier
* Represents a variable from the scope.
*/
export async function resolve({ node, scope }: ResolveNodeProps<Identifier>) {
export async function resolve({
node,
scope,
builtins,
}: ResolveNodeProps<Identifier>) {
if (node.name in builtins) {
return builtins[node.name]!()
}
if (!scope.exists(node.name)) {
return undefined
}
Expand All @@ -19,8 +26,12 @@ export async function resolve({ node, scope }: ResolveNodeProps<Identifier>) {
export function updateScopeContext({
node,
scopeContext,
builtins,
raiseError,
}: UpdateScopeContextProps<Identifier>) {
if (node.name in builtins) {
return
}
if (!scopeContext.definedVariables.has(node.name)) {
if (scopeContext.onlyPredefinedVariables === undefined) {
scopeContext.usedUndefinedVariables.add(node.name)
Expand Down
2 changes: 2 additions & 0 deletions src/compiler/logic/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,13 @@ type RaiseErrorFn<T = void | never> = (
export type ResolveNodeProps<N extends Node> = {
node: N
scope: Scope
builtins: Record<string, () => any>
raiseError: RaiseErrorFn<never>
}

export type UpdateScopeContextProps<N extends Node> = {
node: N
scopeContext: ScopeContext
builtins: Record<string, () => any>
raiseError: RaiseErrorFn<void>
}
27 changes: 27 additions & 0 deletions src/compiler/scan.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -588,6 +588,33 @@ describe('parameters', async () => {

expect(metadata.parameters).toEqual(new Set(['foo', 'bar', 'arr']))
})

it('does not include special identifiers as parameters', async () => {
const prompt = `
{{ $now }}
`

const metadata = await scan({
prompt: removeCommonIndent(prompt),
})

expect(metadata.parameters).toEqual(new Set())
})

it('raises error when assigning to builtin', async () => {
const prompt = `
{{ $now = "2023-01-01" }}
`

const metadata = await scan({
prompt: removeCommonIndent(prompt),
})

expect(metadata.errors).toHaveLength(1)
expect(metadata.errors[0]!.message).toBe(
"Cannot assign to builtin variable: '$now'",
)
})
})

describe('referenced prompts', async () => {
Expand Down
4 changes: 4 additions & 0 deletions src/compiler/scan.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
CUSTOM_MESSAGE_ROLE_ATTR,
REFERENCE_DEPTH_LIMIT,
REFERENCE_PATH_ATTR,
SPECIAL_RESOLVERS,
TAG_NAMES,
} from '$promptl/constants'
import CompileError, { error } from '$promptl/error/error'
Expand Down Expand Up @@ -57,6 +58,7 @@ export class Scan {
private withParameters?: string[]
private requireConfig: boolean
private configSchema?: z.ZodType
private builtins: Record<string, () => any>

private config?: Config
private configPosition?: { start: number; end: number }
Expand Down Expand Up @@ -95,6 +97,7 @@ export class Scan {
this.withParameters = withParameters
this.configSchema = configSchema
this.requireConfig = requireConfig ?? false
this.builtins = SPECIAL_RESOLVERS

this.resolvedPrompt = document.content
this.includedPromptPaths = new Set([this.fullPath])
Expand Down Expand Up @@ -194,6 +197,7 @@ export class Scan {
await updateScopeContextForNode({
node,
scopeContext,
builtins: this.builtins,
raiseError: this.expressionError.bind(this),
})
}
Expand Down
5 changes: 5 additions & 0 deletions src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,3 +40,8 @@ export enum KEYWORDS {

export const RESERVED_KEYWORDS = Object.values(KEYWORDS)
export const RESERVED_TAGS = Object.values(TAG_NAMES)
export const SPECIAL_IDENTIFIERS = new Set(['$now'])

export const SPECIAL_RESOLVERS: Record<string, () => unknown> = {
$now: () => new Date(),
}
4 changes: 4 additions & 0 deletions src/error/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,10 @@ export default {
code: 'invalid-assignment',
message: 'Invalid assignment',
},
assignmentToBuiltin: (name: string) => ({
code: 'assignment-to-builtin',
message: `Cannot assign to builtin variable: '${name}'`,
}),
unknownTag: (name: string) => ({
code: 'unknown-tag',
message: `Unknown tag: '${name}'`,
Expand Down