Skip to content

Commit

Permalink
Allow non-proxied Futures to be used a "bracket" index types
Browse files Browse the repository at this point in the history
  • Loading branch information
liamgriffiths committed Jul 17, 2024
1 parent 3e146ba commit 4f1685c
Show file tree
Hide file tree
Showing 6 changed files with 92 additions and 63 deletions.
76 changes: 49 additions & 27 deletions examples/arbitrary-future-properties.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#!/usr/bin/env -S npx ts-node --transpileOnly

import { Substrate, Box, sb } from "substrate";
import { Substrate, Box, ComputeText, ComputeJSON, sb } from "substrate";

async function main() {
const SUBSTRATE_API_KEY = process.env["SUBSTRATE_API_KEY"];
Expand All @@ -9,37 +9,59 @@ async function main() {
apiKey: SUBSTRATE_API_KEY,
});

const numbers = new Box(
{
value: [0, 1],
},
);
const numbers = new Box({
value: [0, 1],
});

const latin = new Box({
value: ["a", "b"],
});

const latin = new Box(
{
value: ["a", "b"],
const greek = new Box({
value: {
a: "α",
b: "β",
},
);
});

const proxyAccessor = new Box({ value: { property: "text" } });

const d = new ComputeText({
prompt: "What is the character for the Latin 'D' in the Cyrillic alphabet? (just tell me the character only)",
max_tokens: 1,
});

const greek = new Box(
{
value: {
a: "α",
b: "β",
const three = new ComputeText({
prompt: "What number comes after '2'? (just tell me the character only)",
max_tokens: 1,
});
const number3 = sb.jq<number>(three.future, ".text | tonumber");

const hebrew = new ComputeJSON({
prompt: "what are the characters of the Hebrew alphabet (in unicode, eg. א )?",
json_schema: {
type: "object",
properties: {
characters: {
type: "array",
items: {
type: "string",
description: "single character",
}
}
}
},
);

const result = new Box(
{
value: {
x: latin.future.value.xyz,
a: latin.future.value[numbers.future.value[0]],
b: greek.future.value[latin.future.value[1]],
ab: sb.concat(greek.future.value.a, greek.future.value.b),
},
}
);
});

const result = new Box({
value: {
a: latin.future.value[numbers.future.value[0]],
b: greek.future.value[latin.future.value[1]],
c: hebrew.future.json_object.characters[number3 as any],
ab: sb.concat(greek.future.value.a, greek.future.value.b),
d: sb.get<string>(d.future, proxyAccessor.future.value.property),
},
});

const res = await substrate.run(result);
console.log(res.get(result));
Expand Down
6 changes: 3 additions & 3 deletions examples/bug.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,11 @@ async function main() {

const selected = new Box({
value: {
example1: sb.get<string>(
data.future.value.object,
example1: sb.concat(
data.future.value.letters[0],
data.future.value.letters[1],
),
example2: data.future.value.letters[data.future.value.index],
// example2: data.future.value.letters[data.future.value.index],
},
});

Expand Down
11 changes: 10 additions & 1 deletion src/Future.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { idGenerator } from "substrate/idGenerator";
import { Node } from "substrate/Node";
import { unproxy } from "./ProxiedFuture";
import { unproxy, proxyFactory } from "./ProxiedFuture";

type Accessor = "item" | "attr";
type TraceOperation = {
Expand Down Expand Up @@ -222,6 +222,15 @@ export class Future<T = unknown> {
return this._directive.result();
}

[Symbol.toPrimitive]() {
// Because we would like to use `Future` values as accessors with bracket-notation on proxied `Future` values
// we need to ensure that when a `Future` instance is being converted into a primitive (as all values are when
// used with bracket-access are) we track a reference to the value and use a special ID that can be used
// later on in the proxy to look up and use the original `Future` when constructing the `Trace` used in the
// resulting proxied `Future`.
return proxyFactory.futureToPrimitive(this);
}

toJSON() {
return {
id: this._id,
Expand Down
38 changes: 19 additions & 19 deletions src/ProxiedFuture.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,22 @@ export const GET_TARGET = "$$GET_TARGET";
/**
* @internal
*/
export const makeProxyFactory = (futureTable: FutureTable = {}) => {
const makeProxyFactory = (futureTable: FutureTable = {}) => {
// Converts a future into an internal-ID that we use in the `FutureTable`. We're using this special
// format to mitigate potential conflicts with actual user-provided strings.
const futureId = (future: Future<any>): string => {
// @ts-ignore (accessing protected property: _id)
return `${ID_PREFIX}${future._id}`;
};

// Transforms a `Future` into an internal-ID to be used in the `FutureTable` and stores the `Future`
// in this table as a side-effect. Should be called when a `[Symbol.toPrimitive]()` is called on a `Future`.
const futureToPrimitive = (future: Future<any>): string => {
const id = futureId(future);
futureTable[id] = future; // store in lookup table
return id;
}

const makeProxy = <T = unknown>(future: Future<T>): any => {
return new Proxy(future, {
has(target: Future<T>, prop: any) {
Expand All @@ -32,30 +42,19 @@ export const makeProxyFactory = (futureTable: FutureTable = {}) => {
if (prop === GET_TARGET) return target;

if (prop === Symbol.toPrimitive) {
// When the prop is not a primitive (number, string, Symbol) it will be attempted
// to be converted into one. This case will happen when the prop is a Future.
//
// Because only primitive types can be used as property accessors, what we're doing
// here is returning a Future ID, which is a string. We use this specially formatted
// ID to store the original Future in a lookup table we maintain as some hidden
// state in the SDK.
//
// When the prop (Future ID) is used as an accessor (eg. in the case of "bracket"
// access) we will use the returned Future ID here and look up the referenced Future
// when constructing new proxied Futures (in it's TraceProps).
return () => {
const utarget = unproxy(target);
const id = futureId(utarget);
futureTable[id] = utarget; // store in lookup table
return id;
};
// Because we would like to use `Future` values as accessors with bracket-notation on proxied `Future` values
// we need to ensure that when a `Future` instance is being converted into a primitive (as all values are when
// used with bracket-access are) we track a reference to the value and use a special ID that can be used
// later on in the proxy to look up and use the original `Future` when constructing the `Trace` used in the
// resulting proxied `Future`.
return () => futureToPrimitive(unproxy(target));
}

const nextProp = prop.startsWith(ID_PREFIX)
? // When the prop is a Future ID, we will lookup the corresponding Future
// so that we can use it as a TraceProp in the resulting new Future.
futureTable[prop]!
: // Otherwise the prop is not a future (always converted to string)
: // Otherwise the prop is not a future (converted to string at this point)
(prop as string);

// @ts-ignore (access protected prop: _directive)
Expand All @@ -69,6 +68,7 @@ export const makeProxyFactory = (futureTable: FutureTable = {}) => {

return {
makeProxy,
futureToPrimitive,
};
};

Expand Down
4 changes: 4 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,4 +52,8 @@ export {
export { sb } from "substrate/sb";
export { Substrate };
import { Substrate } from "substrate/Substrate";


export { Future, Trace } from "substrate/Future";
export { Node } from "substrate/Node";
export default Substrate;
20 changes: 7 additions & 13 deletions tests/ProxiedFuture.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { expect, describe, test } from "vitest";
import { Future, Trace } from "substrate/Future";
import { Node } from "substrate/Node";
import { makeProxyFactory, isProxy, unproxy } from "substrate/ProxiedFuture";
import { proxyFactory, isProxy, unproxy } from "substrate/ProxiedFuture";

class FooFuture extends Future<any> {}
class FooNode extends Node {}
Expand All @@ -11,15 +11,13 @@ const node = (id: string = "") => new FooNode({}, { id });
describe("ProxiedFuture", () => {
describe("ProxyFactory", () => {
test("makeProxy", () => {
const proxyFactory = makeProxyFactory();
const f = new FooFuture(new Trace([], node()));
const p = proxyFactory.makeProxy(f);
// Proxy is an instance of Future
expect(p).instanceof(Future);
});

test("isProxy", () => {
const proxyFactory = makeProxyFactory();
const f = new FooFuture(new Trace([], node()));
const p = proxyFactory.makeProxy(f);
// We can detect whether the proxied Future is a proxy
Expand All @@ -30,7 +28,6 @@ describe("ProxiedFuture", () => {

describe("Proxy", () => {
test("unproxy (returns unproxied Future)", () => {
const proxyFactory = makeProxyFactory();
const f = new FooFuture(new Trace([], node()));
const p = proxyFactory.makeProxy(f);
const up = unproxy(p);
Expand All @@ -39,16 +36,13 @@ describe("ProxiedFuture", () => {
});

test("arbitrary property access (. notation)", () => {
const proxyFactory = makeProxyFactory();
const f = new FooFuture(new Trace([], node("123")));
const p = proxyFactory.makeProxy(f);

// @ts-ignore (properties don't exist)
const f1 = p.a.b.c;
expect(f1).instanceof(Future);
expect(isProxy(f1)).toEqual(true);

// @ts-ignore ("virtual property" doesn't exist)
const up = unproxy(f1);
const json = up.toJSON();

Expand All @@ -64,7 +58,6 @@ describe("ProxiedFuture", () => {
});

test("arbitrary property and index access (brackets)", () => {
const proxyFactory = makeProxyFactory();
const f = new FooFuture(new Trace([], node("123")));
const p = proxyFactory.makeProxy(f);

Expand All @@ -86,27 +79,28 @@ describe("ProxiedFuture", () => {
});
});

// TODO(liam): I'm not sure why this test doesn't work yet, but it does work in example code
test.skip("using Future values as proxy accessors", () => {
const proxyFactory = makeProxyFactory();
test("using Future values as proxy accessors", () => {
const f = new FooFuture(new Trace([], node("123")));
const p = proxyFactory.makeProxy(f);

const a = new FooFuture(new Trace([], node("456")));
const b = new FooFuture(new Trace([], node("789")));

// non index-types are illegal in the type system, so we're casting to any here.
const f1 = p[a as any];
const f1 = p[a as any][b as any];

expect(f1).instanceof(Future);
expect(isProxy(f1)).toEqual(true);

const up = unproxy(f1);

const json = up.toJSON();

expect(json.directive).toEqual({
op_stack: [
// @ts-ignore
{ accessor: "attr", future_id: a._id, key: null },
// @ts-ignore
{ accessor: "attr", future_id: b._id, key: null },
],
origin_node_id: "123",
type: "trace",
Expand Down

0 comments on commit 4f1685c

Please sign in to comment.