Compare commits
3 Commits
8b7d99393d
...
0376aab672
| Author | SHA1 | Date | |
|---|---|---|---|
| 0376aab672 | |||
| e12b55114e | |||
| d5517f6650 |
14
experiments/reduction-scanner-1.mjs
Normal file
14
experiments/reduction-scanner-1.mjs
Normal file
@@ -0,0 +1,14 @@
|
||||
import { Reduction_Scanner, Reduction_Settings } from '@efforting.tech/rule-processing/reduction-scanner';
|
||||
//import { Sub_Sequence_Rule } from '@efforting.tech/rule-processing/rules';
|
||||
|
||||
|
||||
const rss = Reduction_Settings.load();
|
||||
|
||||
const rs = new Reduction_Scanner(rss);
|
||||
|
||||
rss.rules.push();
|
||||
|
||||
|
||||
const arr = [10, '+', 20, '*', 30];
|
||||
console.log(rs.perform_reduction(arr));
|
||||
console.log(arr);
|
||||
6
planning/schema-vs-data.md
Normal file
6
planning/schema-vs-data.md
Normal file
@@ -0,0 +1,6 @@
|
||||
# Rationale
|
||||
|
||||
Schema is a quite low level feature that is used all over the place while the data package is more of a high level construct.
|
||||
It makes sense to move schemas into their own package.
|
||||
|
||||
Schema uses a lot of informal conditions and will not depend on rule-processing/conditions but one could use those in higher levels if wanted.
|
||||
@@ -11,6 +11,11 @@ export function factory(factory_function, description) {
|
||||
return new Field_Configuration(null, null, factory_function, description);
|
||||
}
|
||||
|
||||
export function instance(type, description) {
|
||||
|
||||
return new Field_Configuration((i) => i instanceof type, null, () => new type(), description);
|
||||
}
|
||||
|
||||
export function typed_value(coercion_function, default_value, description) {
|
||||
return new Field_Configuration(null, coercion_function, () => default_value, description);
|
||||
}
|
||||
@@ -33,6 +38,14 @@ export function typed_required(coercion_function, description) {
|
||||
return new Field_Configuration((value) => value !== undefined, coercion_function, null, description);
|
||||
}
|
||||
|
||||
export function schema(field_schema, default_value, description) {
|
||||
return new Field_Configuration(null, (v) => field_schema.load(v), () => field_schema.load(default_value), description);
|
||||
}
|
||||
|
||||
export function symbol(symbol_set, default_value, description) {
|
||||
return new Field_Configuration(null, (v) => symbol_set.load(v), () => symbol_set.load(default_value), description);
|
||||
}
|
||||
|
||||
export function symbol_set(description_to_name_mapping, description=null) {
|
||||
|
||||
const symbols_by_name = Object.fromEntries(Object.keys(description_to_name_mapping).map(k => [k, Symbol(k)]));
|
||||
|
||||
74
source/rule-processing/contracts.mjs
Normal file
74
source/rule-processing/contracts.mjs
Normal file
@@ -0,0 +1,74 @@
|
||||
import { FPR_State } from './state.mjs';
|
||||
|
||||
export class Abstract_Reduction_Contract {
|
||||
on_create_scanner(scanner) {}
|
||||
on_start_transform(scanner, sequence) {}
|
||||
on_end_transform(scanner, sequence) {}
|
||||
on_reduction_attempted(scanner, sequence) {}
|
||||
on_reduction_made(scanner, sequence) {}
|
||||
|
||||
check_if_done(scanner, sequence) {
|
||||
return false;
|
||||
}
|
||||
|
||||
assert_readiness(scanner, sequence) {}
|
||||
}
|
||||
|
||||
export class Reduction_Contract extends Abstract_Reduction_Contract {
|
||||
check_if_done(scanner, sequence) {
|
||||
return scanner.last_reduction_status === false;
|
||||
}
|
||||
}
|
||||
|
||||
export class FPR_Contract extends Reduction_Contract {
|
||||
constructor(end_condition=null) {
|
||||
super();
|
||||
Object.assign(this, { end_condition });
|
||||
}
|
||||
|
||||
check_if_done(scanner, sequence) {
|
||||
if (this.end_condition) {
|
||||
return this.end_condition(scanner, sequence);
|
||||
} else {
|
||||
return super.check_if_done(scanner, sequence);
|
||||
}
|
||||
}
|
||||
|
||||
on_create_scanner(scanner) {
|
||||
Object.assign(scanner, {
|
||||
cycle_detected: false,
|
||||
state_manager: new FPR_State(),
|
||||
});
|
||||
}
|
||||
|
||||
on_start_transform(scanner, sequence) {
|
||||
super.on_start_transform(scanner, sequence);
|
||||
const state_manager = scanner.state_manager;
|
||||
state_manager.clear_journal();
|
||||
const state = state_manager.gather_state(sequence); //Log initial state
|
||||
state_manager.log_state(state);
|
||||
|
||||
scanner.cycle_detected = false;
|
||||
}
|
||||
|
||||
on_reduction_made(scanner, sequence) {
|
||||
const state_manager = scanner.state_manager;
|
||||
const state = state_manager.gather_state(sequence);
|
||||
|
||||
const pre_count = state_manager.length;
|
||||
state_manager.log_state(state);
|
||||
const post_count = state_manager.length;
|
||||
|
||||
if (pre_count === post_count) {
|
||||
scanner.cycle_detected = true;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
assert_readiness(scanner, sequence) {
|
||||
if (scanner.cycle_detected) {
|
||||
throw new Error('Cycle detected');
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
103
source/rule-processing/reduction-scanner.mjs
Normal file
103
source/rule-processing/reduction-scanner.mjs
Normal file
@@ -0,0 +1,103 @@
|
||||
import { Reduction_Contract, FPR_Contract } from './contracts.mjs';
|
||||
|
||||
|
||||
import * as CF from '@efforting.tech/data/field-configuration-factories';
|
||||
|
||||
|
||||
export const Reduction_Order = new CF.symbol_set({
|
||||
RULE_MAJOR: 'For each rule, scan the full sequence',
|
||||
POSITION_MAJOR: 'For each item in the sequence, try all rules',
|
||||
}, 'Reduction ordering');
|
||||
|
||||
|
||||
export const Reduction_Settings = new CF.Schema({
|
||||
reduction_order: CF.symbol(Reduction_Order, 'RULE_MAJOR'),
|
||||
reduction_contract: CF.instance(Reduction_Contract, 'The reduction contract defines the implementation of the reduction scanner'),
|
||||
rules: CF.factory(() => []),
|
||||
}, 'Reduction settings');
|
||||
|
||||
//TODO: Consider whether we can implement some hierarchical sub schema, like classes - we could of course reuse things by defining and object and spread it in here
|
||||
export const FP_Reduction_Settings = new CF.Schema({
|
||||
reduction_order: CF.symbol(Reduction_Order, 'RULE_MAJOR'),
|
||||
reduction_contract: CF.instance(FPR_Contract, 'The reduction contract defines the implementation of the reduction scanner'),
|
||||
rules: CF.factory(() => []),
|
||||
}, 'Fixed point reduction settings');
|
||||
|
||||
|
||||
|
||||
export class Reduction_Scanner {
|
||||
|
||||
static settings_schema = Reduction_Settings;
|
||||
|
||||
constructor(settings, use_lifecycle_cb=true) {
|
||||
settings = this.constructor.settings_schema.load(settings);
|
||||
Object.assign(this, { settings });
|
||||
this.clear_transform_state();
|
||||
if (use_lifecycle_cb) {
|
||||
settings.reduction_contract.on_create_scanner(this);
|
||||
}
|
||||
}
|
||||
|
||||
perform_reduction(sequence) {
|
||||
const { settings } = this;
|
||||
switch (settings.reduction_order) {
|
||||
|
||||
case Reduction_Order.symbols.RULE_MAJOR:
|
||||
for (const rule of settings.rules) {
|
||||
for (let start_index=0; start_index < sequence.length; start_index++) {
|
||||
const match = rule.match(sequence, start_index);
|
||||
if (match) {
|
||||
rule.action(this, sequence, match);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
|
||||
case Reduction_Order.symbols.POSITION_MAJOR:
|
||||
for (let start_index=0; start_index < sequence.length; start_index++) {
|
||||
for (const rule of settings.rules) {
|
||||
const match = rule.match(sequence, start_index);
|
||||
if (match) {
|
||||
rule.action(this, sequence, match);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
default:
|
||||
throw new Error(`Unknown reduction order: ${this.reduction_order}`); //TODO: Force invalid configuration error
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
clear_transform_state() {
|
||||
this.last_reduction_status = null;
|
||||
this.reductions_attempted = 0;
|
||||
this.reductions_made = 0;
|
||||
}
|
||||
|
||||
transform(sequence) {
|
||||
const { settings } = this;
|
||||
const reduction_contract = settings.reduction_contract;
|
||||
this.clear_transform_state();
|
||||
reduction_contract.on_start_transform(this, sequence);
|
||||
|
||||
while (!reduction_contract.check_if_done(this, sequence)) {
|
||||
reduction_contract.assert_readiness(this, sequence);
|
||||
this.last_reduction_status = this.perform_reduction(sequence);
|
||||
this.reductions_attempted++;
|
||||
if (this.last_reduction_status) {
|
||||
this.reductions_made++;
|
||||
reduction_contract.on_reduction_made(this, sequence);
|
||||
}
|
||||
reduction_contract.on_reduction_attempted(this, sequence);
|
||||
}
|
||||
|
||||
reduction_contract.on_end_transform(this, sequence);
|
||||
return sequence;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Item_Unresolvable } from '@efforting.tech/errors';
|
||||
|
||||
export class Abstract_Resolver {
|
||||
resolve(item, extra_info={}) {
|
||||
const result = this.resolve_handler(item);
|
||||
const result = this.resolve_handler(item, extra_info);
|
||||
if (!result?.handler) {
|
||||
throw new Item_Unresolvable({ resolver: this, item });
|
||||
}
|
||||
@@ -17,10 +17,10 @@ export class Chained_Resolver extends Abstract_Resolver {
|
||||
Object.assign(this, { chain_links });
|
||||
}
|
||||
|
||||
resolve_handler(item) {
|
||||
resolve_handler(item, extra_info={}) {
|
||||
const { chain_links } = this;
|
||||
for (const link of chain_links) {
|
||||
const result = link.resolve_handler(item);
|
||||
const result = link.resolve_handler(item, extra_info);
|
||||
if (result?.handler) {
|
||||
return result;
|
||||
}
|
||||
@@ -35,10 +35,10 @@ export class Predicate_Resolver extends Abstract_Resolver {
|
||||
Object.assign(this, { rules });
|
||||
}
|
||||
|
||||
resolve_handler(item) {
|
||||
resolve_handler(item, extra_info={}) {
|
||||
const { rules } = this;
|
||||
for (const [predicate, handler] of rules) {
|
||||
const predicate_result = predicate(item);
|
||||
const predicate_result = predicate(item, extra_info);
|
||||
// NOTE: to return a falsy predicate_result as a positive hit you must wrap it in something
|
||||
if (predicate_result) {
|
||||
return { handler, predicate_result };
|
||||
@@ -52,7 +52,7 @@ export class RegExp_Resolver extends Predicate_Resolver {
|
||||
// NOTE: Rules should be iterable as [predicate, handler] pairs
|
||||
super();
|
||||
Object.assign(this, {
|
||||
rules: rules.map(([pattern, handler]) => [(str) => str.match(pattern), handler])
|
||||
rules: rules.map(([pattern, handler]) => [(str, extra_info) => str.match(pattern), handler])
|
||||
});
|
||||
}
|
||||
|
||||
@@ -65,7 +65,7 @@ export class Mapping_Resolver extends Abstract_Resolver {
|
||||
Object.assign(this, { rules, key_function });
|
||||
}
|
||||
|
||||
resolve_handler(item) {
|
||||
resolve_handler(item, extra_info={}) {
|
||||
const { key_function, rules } = this;
|
||||
const key = key_function ? key_function(item) : item;
|
||||
return rules.get(key);
|
||||
@@ -73,3 +73,26 @@ export class Mapping_Resolver extends Abstract_Resolver {
|
||||
|
||||
}
|
||||
|
||||
|
||||
export class Sequential_Resolver extends Abstract_Resolver {
|
||||
constructor(rules=[]) {
|
||||
// NOTE: Rules should be iterable as [Abstract_Sequential_Condition, handler] pairs
|
||||
super();
|
||||
Object.assign(this, { rules });
|
||||
}
|
||||
|
||||
|
||||
resolve_handler(item, extra_info={}) {
|
||||
const { rules } = this;
|
||||
|
||||
for (const [sequential_condition, handler] of rules) {
|
||||
const sequential_condition_result = sequential_condition.match(item, extra_info);
|
||||
// NOTE: to return a falsy predicate_result as a positive hit you must wrap it in something
|
||||
if (sequential_condition_result) {
|
||||
return { handler, sequential_condition_result };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
134
source/rule-processing/state-serializer.mjs
Normal file
134
source/rule-processing/state-serializer.mjs
Normal file
@@ -0,0 +1,134 @@
|
||||
import * as CF from '@efforting.tech/data/field-configuration-factories';
|
||||
|
||||
|
||||
export const Object_Serialization_Strategy = new CF.Schema({
|
||||
identity: CF.boolean(true, 'Serialize based on identity'),
|
||||
type: CF.boolean(true, 'Serialize based on constructor'),
|
||||
symbols: CF.boolean(true, 'Include symbol-keyed properties'),
|
||||
keys: CF.boolean(true, 'Include property keys'),
|
||||
values: CF.boolean(true, 'Include property values'),
|
||||
}, 'Object serialization strategy');
|
||||
|
||||
|
||||
|
||||
export const State_Serialization_Strategy = new CF.Schema({
|
||||
//BUG - there is currently no way (I think) to put defaults into a sub schema - this should be fixed
|
||||
array: CF.schema(Object_Serialization_Strategy, {
|
||||
identity: false,
|
||||
type: true,
|
||||
symbols: false,
|
||||
keys: false,
|
||||
values: true,
|
||||
}, 'State serialization strategy for Array'),
|
||||
object: CF.schema(Object_Serialization_Strategy, {
|
||||
identity: true,
|
||||
type: true,
|
||||
symbols: true,
|
||||
keys: true,
|
||||
values: true,
|
||||
}, 'State serialization strategy for Object'),
|
||||
}, 'State serialization strategy');
|
||||
|
||||
|
||||
|
||||
|
||||
export class State_Serializer {
|
||||
|
||||
static settings_schema = State_Serialization_Strategy;
|
||||
|
||||
constructor(symbols=new Map(), strategy) {
|
||||
strategy = this.constructor.settings_schema.load(strategy);
|
||||
Object.assign(this, { symbols, strategy });
|
||||
}
|
||||
|
||||
serialize_symbols(...symbols) {
|
||||
const result = [];
|
||||
for (const symbol of symbols) {
|
||||
const existing = this.symbols.get(symbol);
|
||||
if (existing !== undefined) {
|
||||
result.push(existing);
|
||||
} else {
|
||||
const new_symbol_index = this.symbols.size;
|
||||
this.symbols.set(symbol, new_symbol_index);
|
||||
result.push(new_symbol_index);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
serialize_object(object) {
|
||||
const state_strategy = this.strategy;
|
||||
const result = [];
|
||||
|
||||
switch (typeof(object)) {
|
||||
case 'object':
|
||||
if (object === null) {
|
||||
return ['N'];
|
||||
}
|
||||
|
||||
|
||||
const is_array = object.constructor === Array;
|
||||
const strategy = is_array ? state_strategy.array : state_strategy.object;
|
||||
result.push(is_array ? 'a' : 'o');
|
||||
|
||||
|
||||
if (strategy.identity) {
|
||||
const existing_instance = this.symbols.get(object);
|
||||
if (existing_instance !== undefined) {
|
||||
return existing_instance;
|
||||
}
|
||||
|
||||
const [instance] = this.serialize_symbols(object);
|
||||
result.push(instance);
|
||||
}
|
||||
|
||||
if (strategy.type) {
|
||||
const [type] = this.serialize_symbols(object.constructor);
|
||||
result.push(type);
|
||||
}
|
||||
|
||||
if (strategy.symbols) {
|
||||
const symbols = this.serialize_symbols(...Object.getOwnPropertySymbols(object));
|
||||
result.push(symbols);
|
||||
}
|
||||
|
||||
if (strategy.keys) {
|
||||
const keys = Object.keys(object).map(item => this.serialize_object(item));
|
||||
result.push(keys);
|
||||
}
|
||||
|
||||
if (strategy.values) {
|
||||
const values = Object.values(object).map(item => this.serialize_object(item));
|
||||
result.push(values);
|
||||
}
|
||||
|
||||
return result;
|
||||
|
||||
case 'string':
|
||||
return ['s', ...this.serialize_symbols(typeof(object)), object];
|
||||
|
||||
case 'number':
|
||||
return ['n', ...this.serialize_symbols(typeof(object)), object];
|
||||
|
||||
case 'boolean':
|
||||
return [object ? 'T' : 'F'];
|
||||
|
||||
case 'function':
|
||||
const [reference_type, reference_id] = this.serialize_symbols(typeof(object), object);
|
||||
return ['f', reference_type, reference_id];
|
||||
|
||||
case 'symbol':
|
||||
return ['S', ...this.serialize_symbols(object)];
|
||||
|
||||
|
||||
case 'undefined':
|
||||
return ['u'];
|
||||
|
||||
|
||||
default:
|
||||
throw new Error(typeof(object)); //TODO - proper error
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
47
source/rule-processing/state.mjs
Normal file
47
source/rule-processing/state.mjs
Normal file
@@ -0,0 +1,47 @@
|
||||
import { State_Serializer } from './state-serializer.mjs';
|
||||
import { createHash } from 'node:crypto';
|
||||
|
||||
|
||||
export class Abstract_State {
|
||||
constructor(seen = new Set()) {
|
||||
Object.assign(this, { seen });
|
||||
}
|
||||
|
||||
gather_state(sequence) {
|
||||
throw new Error(`gather_state(sequence) not implemented for ${this.constructor.name}`);
|
||||
}
|
||||
|
||||
seen_state(state) {
|
||||
return this.seen.has(state);
|
||||
}
|
||||
|
||||
log_state(state) {
|
||||
this.seen.add(state);
|
||||
}
|
||||
|
||||
discard_state(state) {
|
||||
this.seen.delete(state);
|
||||
}
|
||||
|
||||
get length() {
|
||||
return this.seen.size;
|
||||
}
|
||||
|
||||
clear_journal() {
|
||||
this.seen.clear();
|
||||
}
|
||||
}
|
||||
|
||||
export class FPR_State extends Abstract_State {
|
||||
constructor(seen = new Set(), serializer = new State_Serializer()) {
|
||||
super(seen);
|
||||
Object.assign(this, { serializer });
|
||||
}
|
||||
|
||||
gather_state(sequence) {
|
||||
const str = JSON.stringify(this.serializer.serialize_object(sequence));
|
||||
const hash = createHash('sha1').update(str).digest('hex');
|
||||
return `${hash.slice(0, 8)}-${str}`;
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user