From 0376aab6726a68a4e0d72ee7bacc8f778ac4d422 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mikael=20L=C3=B6vqvist?= Date: Sun, 19 Apr 2026 21:05:10 +0200 Subject: [PATCH] Started working on reduction system --- experiments/reduction-scanner-1.mjs | 14 ++ source/rule-processing/contracts.mjs | 74 ++++++++++ source/rule-processing/reduction-scanner.mjs | 103 ++++++++++++++ source/rule-processing/resolvers.mjs | 37 ++++- source/rule-processing/state-serializer.mjs | 134 +++++++++++++++++++ source/rule-processing/state.mjs | 47 +++++++ 6 files changed, 402 insertions(+), 7 deletions(-) create mode 100644 experiments/reduction-scanner-1.mjs create mode 100644 source/rule-processing/contracts.mjs create mode 100644 source/rule-processing/reduction-scanner.mjs create mode 100644 source/rule-processing/state-serializer.mjs create mode 100644 source/rule-processing/state.mjs diff --git a/experiments/reduction-scanner-1.mjs b/experiments/reduction-scanner-1.mjs new file mode 100644 index 0000000..3bb1ac9 --- /dev/null +++ b/experiments/reduction-scanner-1.mjs @@ -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); \ No newline at end of file diff --git a/source/rule-processing/contracts.mjs b/source/rule-processing/contracts.mjs new file mode 100644 index 0000000..d75d64e --- /dev/null +++ b/source/rule-processing/contracts.mjs @@ -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'); + } + } + +} diff --git a/source/rule-processing/reduction-scanner.mjs b/source/rule-processing/reduction-scanner.mjs new file mode 100644 index 0000000..b5d4a08 --- /dev/null +++ b/source/rule-processing/reduction-scanner.mjs @@ -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; + } + +} + diff --git a/source/rule-processing/resolvers.mjs b/source/rule-processing/resolvers.mjs index c098757..cba1d76 100644 --- a/source/rule-processing/resolvers.mjs +++ b/source/rule-processing/resolvers.mjs @@ -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 }; + } + } + } + +} + diff --git a/source/rule-processing/state-serializer.mjs b/source/rule-processing/state-serializer.mjs new file mode 100644 index 0000000..42fcba1 --- /dev/null +++ b/source/rule-processing/state-serializer.mjs @@ -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 + } + } +}; + diff --git a/source/rule-processing/state.mjs b/source/rule-processing/state.mjs new file mode 100644 index 0000000..1a39fbf --- /dev/null +++ b/source/rule-processing/state.mjs @@ -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}`; + } + +} \ No newline at end of file