aran
Version:
JavaScript instrumenter
530 lines (515 loc) • 15.6 kB
TypeScript
import type {
DeclareHeader,
Header,
ModuleHeader,
} from "../../lang/header.d.ts";
import type {
Atom,
Intrinsic,
Parameter,
ResolvePartialAtom,
RuntimePrimitive,
} from "../../lang/syntax.d.ts";
import type { GetDefault, ValueOf } from "../../util/util.d.ts";
import type {
ControlKind,
ProgramKind,
ClosureKind,
SegmentKind,
GeneratorKind,
Parametrization,
} from "../parametrization.d.ts";
export type TestKind = "if" | "while" | "conditional";
export type Frame<variable extends string, value> = {
[key in variable | Parameter]?: value;
};
/**
* By including the kind of the block as the type of the frame, we remove its
* parametrization on block kind yet retain precise type information about which
* parameter is present in the frame.
*
* ```ts
* const standalone_frame: StandaloneFrame = { type: kind, data: frame };
* ```
*/
export type PreciseFrame<variable extends string, value> = ValueOf<{
[K in ControlKind]: {
type: K;
data: {
[key in variable]?: variable;
} & {
[key in Parametrization[K]]: value;
};
};
}>;
export type PreciseHeader<kind extends ProgramKind> = kind extends "module"
? ModuleHeader
: kind extends "script" | "eval.global" | "eval.glocal.root"
? DeclareHeader
: kind extends "eval.local.deep"
? never
: never;
export type Runtime = {
State: unknown;
StackValue: unknown;
ScopeValue: unknown;
OtherValue: unknown;
};
/**
* The standard weaving API expects a global value at
* `config.advice_variable` that holds all the advice functions. It is simpler
* to use than the flexible weaving API but it does let the user define the
* static information provided to the advice functions.
*/
export type AspectTyping<atom extends Atom, runtime extends Runtime> = {
/**
* The first advice called upon entering any block. It provides an oportunity
* to overwrite the state that other advices will receive. That is that it
* receives the state of the parent block and returns the state that will be
* passed to the other advice of this block. If the block is the root block
* -- ie a program block -- it will receive a clone of `config.initial_state`.
*/
"block@setup": {
pointcut: (kind: ControlKind, tag: atom["Tag"]) => boolean;
advice: (
state: runtime["State"],
kind: ControlKind,
tag: atom["Tag"],
) => runtime["State"];
};
/**
* Called before entering a program block with the headers of the program.
*/
"program-block@before": {
pointcut: (kind: ProgramKind, tag: atom["Tag"]) => boolean;
advice: (
state: runtime["State"],
kind: ProgramKind,
head: Header[],
tag: atom["Tag"],
) => void;
};
/**
* Called before entering a closure block.
*/
"closure-block@before": {
pointcut: (kind: ClosureKind, tag: atom["Tag"]) => boolean;
advice: (
state: runtime["State"],
kind: ClosureKind,
tag: atom["Tag"],
) => void;
};
/**
* Called before entering a segment block with the labels of the current
* block.
*/
"segment-block@before": {
pointcut: (kind: SegmentKind, tag: atom["Tag"]) => boolean;
advice: (
state: runtime["State"],
kind: SegmentKind,
labels: atom["Label"][],
tag: atom["Tag"],
) => void;
};
/**
* Called before entering any block. It provides the initial values of the
* scope frame of the current block. Parameters such as `catch.error` may have
* an arbitrary initial value but regular variables can initially only be
* `undefined` or the intrinsic symbol `aran.deadzone_symbol`.
*/
"block@declaration": {
pointcut: (kind: ControlKind, tag: atom["Tag"]) => boolean;
advice: (
state: runtime["State"],
kind: ControlKind,
frame: Frame<atom["Variable"], runtime["ScopeValue"]>,
tag: atom["Tag"],
) => void;
};
/**
* Same as `block@declaration` but it provides an opportunity to overwrite the
* initial values of the scope frame of the current block. The advice
* `block@declaration` does not provide this capability for performance
* reasons.
*/
"block@declaration-overwrite": {
pointcut: (kind: ControlKind, tag: atom["Tag"]) => boolean;
advice: <kind extends ControlKind>(
state: runtime["State"],
kind: kind,
frame: Frame<atom["Variable"], runtime["ScopeValue"]>,
tag: atom["Tag"],
) => Frame<atom["Variable"], runtime["ScopeValue"]>;
};
/**
* Called right before leaving the head of a generator function. That is right
* before the generator returns its iterator. This advice will not be called
* if the head of the generator threw an error. Note that the head and the
* body of a generator are considered to be part of the same block.
*/
"generator-block@suspension": {
pointcut: (kind: GeneratorKind, tag: atom["Tag"]) => boolean;
advice: (
state: runtime["State"],
kind: GeneratorKind,
tag: atom["Tag"],
) => void;
};
/**
* Called right after the first call to the `next` method of the iterator
* returned by a generator. Note that the head and the body of a generator are
* considered to be part of the same block.
*/
"generator-block@resumption": {
pointcut: (kind: GeneratorKind, tag: atom["Tag"]) => boolean;
advice: (
state: runtime["State"],
kind: GeneratorKind,
tag: atom["Tag"],
) => void;
};
/**
* Called before leaving a program block with its return value. If an error
* was thrown, this advice will not be called.
*/
"program-block@after": {
pointcut: (kind: ProgramKind, tag: atom["Tag"]) => boolean;
advice: (
state: runtime["State"],
kind: ProgramKind,
value: runtime["StackValue"],
tag: atom["Tag"],
) => runtime["OtherValue"];
};
/**
* Called before leaving a closure block with its completion value. If an
* error was thrown, this advice will not be called.
*/
"closure-block@after": {
pointcut: (kind: ClosureKind, tag: atom["Tag"]) => boolean;
advice: (
state: runtime["State"],
kind: ClosureKind,
value: runtime["StackValue"],
tag: atom["Tag"],
) => runtime["OtherValue"];
};
/**
* Called before leaving a control block. If an error was thrown or if a label
* was broken onto, this advice will not be called.
*/
"segment-block@after": {
pointcut: (kind: SegmentKind, tag: atom["Tag"]) => boolean;
advice: (
state: runtime["State"],
kind: SegmentKind,
tag: atom["Tag"],
) => void;
};
/**
* Called before leaving any block only if an error was thrown.
*/
"block@throwing": {
pointcut: (kind: ControlKind, tag: atom["Tag"]) => boolean;
advice: (
state: runtime["State"],
kind: ControlKind,
value: runtime["OtherValue"],
tag: atom["Tag"],
) => void;
};
/**
* Called right before leaving any block regardless of how it terminated.
*/
"block@teardown": {
pointcut: (kind: ControlKind, tag: atom["Tag"]) => boolean;
advice: (
state: runtime["State"],
kind: ControlKind,
tag: atom["Tag"],
) => void;
};
/**
* Called right before evaluating a break statement.
*/
"break@before": {
pointcut: (label: atom["Label"], tag: atom["Tag"]) => boolean;
advice: (
state: runtime["State"],
label: atom["Label"],
tag: atom["Tag"],
) => void;
};
/**
* Called right before using a value as a boolean to branch the control flow.
*/
"test@before": {
pointcut: (kind: TestKind, tag: atom["Tag"]) => boolean;
advice: (
state: runtime["State"],
kind: TestKind,
value: runtime["StackValue"],
tag: atom["Tag"],
) => runtime["OtherValue"];
};
/**
* Called right after an intrinsic value was read.
*/
"intrinsic@after": {
pointcut: (name: Intrinsic, tag: atom["Tag"]) => boolean;
advice: (
state: runtime["State"],
name: Intrinsic,
value: runtime["OtherValue"],
tag: atom["Tag"],
) => runtime["StackValue"];
};
/**
* Called right after a primitive value was created.
*/
"primitive@after": {
pointcut: (primitive: RuntimePrimitive, tag: atom["Tag"]) => boolean;
advice: (
state: runtime["State"],
value: RuntimePrimitive & runtime["OtherValue"],
tag: atom["Tag"],
) => runtime["StackValue"];
};
/**
* Called right after a value was imported from another module.
*/
"import@after": {
pointcut: (
source: atom["Source"],
specifier: atom["Specifier"] | null,
tag: atom["Tag"],
) => boolean;
advice: (
state: runtime["State"],
source: atom["Source"],
specifier: atom["Specifier"] | null,
value: runtime["OtherValue"],
tag: atom["Tag"],
) => runtime["StackValue"];
};
/**
* Called right after a closure was created. We use the term 'closure' because
* we reserve the term 'function' for actual regular functions.
*/
"closure@after": {
pointcut: (kind: ClosureKind, tag: atom["Tag"]) => boolean;
advice: (
state: runtime["State"],
kind: ClosureKind,
closure: runtime["OtherValue"] & Function,
tag: atom["Tag"],
) => runtime["StackValue"];
};
/**
* Called right after a value was read from the current scope. The variable is
* guaranteed to exist in the current scope.
*/
"read@after": {
pointcut: (
identifier: Parameter | atom["Variable"],
tag: atom["Tag"],
) => boolean;
advice: (
state: runtime["State"],
identifier: Parameter | atom["Variable"],
value: runtime["ScopeValue"],
tag: atom["Tag"],
) => runtime["StackValue"];
};
/**
* Called right before a value will be used as code to a direct eval call.
* Supporting direct eval calls entails instrumenting this value. Otherwise,
* this code will interact very poorly with the surrounding instrumented code.
*/
"eval@before": {
pointcut: (tag: atom["Tag"]) => boolean;
advice: (
state: runtime["State"],
value: runtime["StackValue"],
tag: atom["Tag"],
) => runtime["OtherValue"];
};
/**
* Called right after returning from a direct eval call.
*/
"eval@after": {
pointcut: (tag: atom["Tag"]) => boolean;
advice: (
state: runtime["State"],
value: runtime["OtherValue"],
tag: atom["Tag"],
) => runtime["StackValue"];
};
/**
* Called right before a value will be used as a promise in a `await`
* expression. That is that all the asynchronous closures at the top of the
* callstack will be stashed away.
*/
"await@before": {
pointcut: (tag: atom["Tag"]) => boolean;
advice: (
state: runtime["State"],
value: runtime["StackValue"],
tag: atom["Tag"],
) => runtime["OtherValue"];
};
/**
* Called right after a promise used inside an `await` expression successfully
* resolved. That is that the asynchronous closure stack will be restored.
*/
"await@after": {
pointcut: (tag: atom["Tag"]) => boolean;
advice: (
state: runtime["State"],
value: runtime["OtherValue"],
tag: atom["Tag"],
) => runtime["StackValue"];
};
/**
* Called right before a value will be used as an item in a `yield`
* expression. That is that the current call will be stashed away.
*/
"yield@before": {
pointcut: (delegate: boolean, tag: atom["Tag"]) => boolean;
advice: (
state: runtime["State"],
delegate: boolean,
value: runtime["StackValue"],
tag: atom["Tag"],
) => runtime["OtherValue"];
};
/**
* Called right after calling the `next` method of the iterator returned by a
* generator. That is that the generator call will be restored.
*/
"yield@after": {
pointcut: (delegate: boolean, tag: atom["Tag"]) => boolean;
advice: (
state: runtime["State"],
delegate: boolean,
value: runtime["OtherValue"],
tag: atom["Tag"],
) => runtime["StackValue"];
};
/**
* Called right after a value will actually *not* be used. For instance an
* expression statement will trigger this advice because the value of the
* expression has no use.
*/
"drop@before": {
pointcut: (tag: atom["Tag"]) => boolean;
advice: (
state: runtime["State"],
value: runtime["StackValue"],
tag: atom["Tag"],
) => runtime["OtherValue"];
};
/**
* Called right before a value will be exported from the current module.
*/
"export@before": {
pointcut: (specifier: atom["Specifier"], tag: atom["Tag"]) => boolean;
advice: (
state: runtime["State"],
specifier: atom["Specifier"],
value: runtime["StackValue"],
tag: atom["Tag"],
) => runtime["OtherValue"];
};
/**
* Called right before a value will be used to update the current scope. The
* variable is guaranteed to exist in the current scope.
*/
"write@before": {
pointcut: (
identifier: Parameter | atom["Variable"],
tag: atom["Tag"],
) => boolean;
advice: (
state: runtime["State"],
identifier: Parameter | atom["Variable"],
value: runtime["StackValue"],
tag: atom["Tag"],
) => runtime["ScopeValue"];
};
/**
* Called in place of a closure regular call. The `this` argument has been
* made explicit and the operation should be performed with `Reflect.apply`.
*/
"apply@around": {
pointcut: (tag: atom["Tag"]) => boolean;
advice: (
state: runtime["State"],
callee: runtime["StackValue"],
this_: runtime["StackValue"],
arguments_: runtime["StackValue"][],
tag: atom["Tag"],
) => runtime["StackValue"];
};
/**
* Called in place of a closure construct call. The operation should be
* performed with `Reflect.construct`.
*/
"construct@around": {
pointcut: (tag: atom["Tag"]) => boolean;
advice: (
state: runtime["State"],
callee: runtime["StackValue"],
arguments_: runtime["StackValue"][],
tag: atom["Tag"],
) => runtime["StackValue"];
};
};
type ResolvePartialRuntime<runtime extends Partial<Runtime>> = {
State: GetDefault<runtime, "State", null>;
StackValue: GetDefault<runtime, "StackValue", unknown>;
ScopeValue: GetDefault<runtime, "ScopeValue", unknown>;
OtherValue: GetDefault<runtime, "OtherValue", unknown>;
};
export type AspectKind = keyof AspectTyping<never, never>;
export type Advice<
param extends Partial<Atom> & {
Kind?: AspectKind;
State?: unknown;
StackValue?: unknown;
ScopeValue?: unknown;
OtherValue?: unknown;
} = {},
> = param extends { Kind: AspectKind }
? {
[key in param["Kind"]]: AspectTyping<
ResolvePartialAtom<param>,
ResolvePartialRuntime<param>
>[key]["advice"];
}
: {
[key in AspectKind]?:
| null
| undefined
| AspectTyping<
ResolvePartialAtom<param>,
ResolvePartialRuntime<param>
>[key]["advice"];
};
export type ObjectPointcut<atom extends Partial<Atom> = {}> = {
[key in AspectKind]?:
| null
| undefined
| boolean
| AspectTyping<ResolvePartialAtom<atom>, never>[key]["pointcut"];
};
export type ConstantPointcut = boolean;
export type ArrayPointcut = AspectKind[];
export type ArrowPointcut<T> = (kind: AspectKind, tag: T) => boolean;
export type Pointcut<atom extends Partial<Atom> = {}> =
| ObjectPointcut<atom>
| ArrowPointcut<GetDefault<atom, "Tag", string>>
| ArrayPointcut
| ConstantPointcut;