@cloudpss/expression 0.6.0-alpha.15 → 0.6.0-alpha.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/dist/definitions/argument.d.ts +1 -1
  2. package/dist/definitions/argument.d.ts.map +1 -1
  3. package/dist/definitions/parameter.d.ts.map +1 -1
  4. package/dist/definitions/parameter.js.map +1 -1
  5. package/dist/definitions/utils.d.ts +2 -1
  6. package/dist/definitions/utils.d.ts.map +1 -1
  7. package/dist/definitions/utils.js +24 -7
  8. package/dist/definitions/utils.js.map +1 -1
  9. package/dist/main.d.ts +1 -1
  10. package/dist/main.d.ts.map +1 -1
  11. package/dist/main.js +25 -17
  12. package/dist/main.js.map +1 -1
  13. package/dist/migrator/call.d.ts.map +1 -1
  14. package/dist/migrator/call.js +39 -13
  15. package/dist/migrator/call.js.map +1 -1
  16. package/dist/migrator/node.d.ts.map +1 -1
  17. package/dist/migrator/node.js +21 -1
  18. package/dist/migrator/node.js.map +1 -1
  19. package/dist/migrator/operator.d.ts.map +1 -1
  20. package/dist/migrator/operator.js +56 -26
  21. package/dist/migrator/operator.js.map +1 -1
  22. package/dist/migrator/symbol.js +2 -2
  23. package/dist/migrator/symbol.js.map +1 -1
  24. package/dist/migrator/to-type.js +2 -2
  25. package/dist/migrator/to-type.js.map +1 -1
  26. package/dist/type.d.ts +22 -10
  27. package/dist/type.d.ts.map +1 -1
  28. package/dist/type.js +19 -26
  29. package/dist/type.js.map +1 -1
  30. package/package.json +3 -3
  31. package/src/definitions/argument.ts +1 -1
  32. package/src/definitions/parameter.ts +15 -8
  33. package/src/definitions/utils.ts +29 -13
  34. package/src/main.ts +21 -14
  35. package/src/migrator/call.ts +37 -15
  36. package/src/migrator/node.ts +22 -1
  37. package/src/migrator/operator.ts +55 -26
  38. package/src/migrator/symbol.ts +2 -2
  39. package/src/migrator/to-type.ts +2 -2
  40. package/src/type.ts +36 -27
  41. package/tests/condition.ts +20 -12
  42. package/tests/definition.ts +12 -10
  43. package/tests/import.ts +3 -3
  44. package/tests/migrate.ts +49 -9
  45. package/tests/scope.ts +3 -3
@@ -1,6 +1,6 @@
1
1
  import { klona } from 'klona';
2
- import { isVmArray, type VmPrimitive, type VmValue } from '@mirascript/mirascript';
3
- import { Expression, isExpression } from '../expression.js';
2
+ import { isVmArray, isVmRecord, type VmPrimitive, type VmValue } from '@mirascript/mirascript';
3
+ import { Expression, isExpression, type ExpressionOrValue } from '../expression.js';
4
4
  import type { ChoiceParameterType, Parameter, ParameterMap } from './parameter.js';
5
5
  import { TypeInfo } from '../type.js';
6
6
  import type { ArgumentMap, ArgumentValue } from './argument.js';
@@ -11,7 +11,9 @@ const { isArray } = Array;
11
11
  export function enumType(definition: Pick<ParameterMap[ChoiceParameterType], 'type' | 'choices'>): TypeInfo | null {
12
12
  const { choices } = definition;
13
13
  if (typeof choices == 'function' || isExpression(choices)) return null;
14
+ // 首先根据参数类型确定默认类型
14
15
  let defaultType: TypeInfo = definition.type === 'logical' ? 'boolean' : 'number';
16
+ // 然后根据默认值调整默认类型
15
17
  if ('value' in definition) {
16
18
  const { value } = definition;
17
19
  if (typeof value == 'boolean') defaultType = 'boolean';
@@ -23,6 +25,7 @@ export function enumType(definition: Pick<ParameterMap[ChoiceParameterType], 'ty
23
25
  let type: TypeInfo | null = null;
24
26
  for (const choice of choices) {
25
27
  const v = choice?.key;
28
+ // 选项值为空时,使用默认类型
26
29
  const t = v == null ? defaultType : typeof v;
27
30
  if (t === 'string') {
28
31
  if (type == null) type = 'string';
@@ -42,15 +45,18 @@ export function enumType(definition: Pick<ParameterMap[ChoiceParameterType], 'ty
42
45
 
43
46
  /**
44
47
  * 转换参数值的类型,不回退到 {@link Parameter.value} 提供的默认值
48
+ * 转换失败时返回 null
45
49
  */
46
- export function toArgumentValue<V extends ArgumentValue>(value: VmValue, definition: Parameter): V {
50
+ export function toArgumentValue<V extends ArgumentValue>(
51
+ value: VmValue | null | undefined,
52
+ definition: Parameter,
53
+ ): V | null {
47
54
  // 避免无效的 VmValue 导致错误
48
55
  value ??= null;
49
-
50
56
  switch (definition.type) {
51
57
  case 'real':
52
58
  case 'integer':
53
- return TypeInfo.toNumber(value) as V;
59
+ return TypeInfo.toNumber(value, null) as V;
54
60
  case 'text':
55
61
  case 'file':
56
62
  case 'code':
@@ -58,16 +64,16 @@ export function toArgumentValue<V extends ArgumentValue>(value: VmValue, definit
58
64
  case 'cssColor' as never:
59
65
  case 'cssBackground' as never:
60
66
  case 'resourceId' as never:
61
- return TypeInfo.toString(value) as V;
67
+ return TypeInfo.toString(value, null) as V;
62
68
 
63
69
  case 'choice':
64
70
  case 'logical': {
65
71
  if (value == null && Array.isArray(definition.choices) && definition.choices.length > 0) {
66
- value = definition.choices[0]?.key;
72
+ value = definition.choices[0]?.key as V;
67
73
  }
68
74
  const type = enumType(definition);
69
75
  if (!type) return value as V;
70
- return TypeInfo.to(value, type) as V;
76
+ return TypeInfo.to(value, type, null) as V;
71
77
  }
72
78
 
73
79
  case 'multiSelect': {
@@ -75,7 +81,13 @@ export function toArgumentValue<V extends ArgumentValue>(value: VmValue, definit
75
81
  if (v.length === 0) return v as V;
76
82
  const type = enumType(definition);
77
83
  if (!type) return v as V;
78
- return v.map((x) => TypeInfo.to(x, type)) as readonly VmPrimitive[] as V;
84
+ const to = TypeInfo.converter(type);
85
+ const ret: VmPrimitive[] = [];
86
+ for (const x of v) {
87
+ const r = to(x, null);
88
+ if (r != null) ret.push(r);
89
+ }
90
+ return ret as readonly VmPrimitive[] as V;
79
91
  }
80
92
 
81
93
  case 'table': {
@@ -84,7 +96,10 @@ export function toArgumentValue<V extends ArgumentValue>(value: VmValue, definit
84
96
  }
85
97
 
86
98
  case 'grouped':
87
- case 'record':
99
+ case 'record': {
100
+ if (!isVmRecord(value)) return null;
101
+ return value as V;
102
+ }
88
103
  default:
89
104
  return value as V;
90
105
  }
@@ -127,7 +142,7 @@ export function fillArgumentMap<T extends Record<string, ArgumentValue>>(
127
142
  if (!checkParameterDefinitions(definition)) return args;
128
143
 
129
144
  /** 参数键的类型 */
130
- type K = keyof T & string;
145
+ type K = keyof T;
131
146
  /** 参数值的类型 */
132
147
  type V = NonNullable<T[K]>;
133
148
  const keys = new Set(Object.keys(args) as K[]);
@@ -140,13 +155,13 @@ export function fillArgumentMap<T extends Record<string, ArgumentValue>>(
140
155
  const currentValue = keys.has(key) ? args[key] : undefined;
141
156
  if (currentValue == null) {
142
157
  // 使用默认值填充
143
- const defaultValue = param.value;
158
+ const defaultValue = param.value as ExpressionOrValue<V>;
144
159
  if (isExpression(defaultValue)) {
145
160
  args[key] = Expression<V>(defaultValue.source);
146
161
  } else {
147
162
  const value =
148
163
  defaultValue != null && typeof defaultValue == 'object' ? klona(defaultValue) : defaultValue;
149
- args[key] = toArgumentValue<V>(value, param);
164
+ args[key] = toArgumentValue<V>(value, param) ?? value;
150
165
  }
151
166
  } else {
152
167
  // 已存在的参数值
@@ -167,6 +182,7 @@ export function fillArgumentMap<T extends Record<string, ArgumentValue>>(
167
182
  // 删除多余的参数
168
183
  for (const key of keys) {
169
184
  // 保留特殊参数和私有字段
185
+ if (typeof key != 'string') continue;
170
186
  const k0 = key[0];
171
187
  if (k0 === '_' || k0 === '$' || k0 === '@' || k0 === 'ɵ') continue;
172
188
  delete args[key];
package/src/main.ts CHANGED
@@ -1,11 +1,11 @@
1
1
  import { isVmFunction, isVmPrimitive, wrapToVmValue, type VmValue } from '@mirascript/mirascript';
2
- import { lib } from '@mirascript/mirascript/subtle';
2
+ import { serialize } from '@mirascript/mirascript/subtle';
3
3
  import { defaultOptions, type Logger, type Options } from './interface.js';
4
4
  import { parse } from './parser.js';
5
5
  import { analyze, type AccessChain } from './analyze.js';
6
6
  import {
7
7
  type CompiledExpression,
8
- Expression,
8
+ type Expression,
9
9
  type ExpressionFunction,
10
10
  type ExpressionOrValue,
11
11
  type ExpressionSource,
@@ -30,7 +30,6 @@ import {
30
30
  } from './definitions.js';
31
31
  const { isArray } = Array;
32
32
  const { defineProperty } = Object;
33
- const { to_string } = lib;
34
33
 
35
34
  /** 选项类型 */
36
35
  export type EvaluatedChoices<T extends ChoiceParameterType> = {
@@ -80,12 +79,17 @@ export class Evaluator {
80
79
  const value = parse(this, expression.source, throws, false);
81
80
  const tag = expression[ExpressionTag];
82
81
  const type = tag ? TypeInfo.parse(tag) : '';
83
- const compiled: ExpressionFunction<T> = (scope, evaluator) => {
84
- const ret = evaluate<T>(evaluator, value, scope);
85
- if (ret == null) return null;
86
- if (!type) return ret;
87
- return TypeInfo.to(ret, type) as T;
88
- };
82
+ let compiled: ExpressionFunction<T>;
83
+ if (!type) {
84
+ compiled = (scope, evaluator) => evaluate<T>(evaluator, value, scope);
85
+ } else {
86
+ const to = TypeInfo.converter(type);
87
+ compiled = (scope, evaluator) => {
88
+ const ret = evaluate<T>(evaluator, value, scope);
89
+ if (ret == null) return null;
90
+ return to(ret) as T;
91
+ };
92
+ }
89
93
  defineProperty(compiled, ExpressionTag, { value: type, enumerable: true });
90
94
  defineProperty(compiled, 'source', { value: value.source, enumerable: true });
91
95
  return compiled as CompiledExpression<T>;
@@ -98,7 +102,7 @@ export class Evaluator {
98
102
  const value = parse(this, template, throws, true);
99
103
  const result = evaluate<string>(this, value, scope ?? this.scope) ?? template;
100
104
  if (typeof result == 'string') return result;
101
- return to_string(result);
105
+ return TypeInfo.toString(result);
102
106
  }
103
107
 
104
108
  /** 求值 */
@@ -190,14 +194,15 @@ export class Evaluator {
190
194
  scope: Scope | null | undefined,
191
195
  defaults = true,
192
196
  ): boolean {
193
- if (typeof condition == 'boolean') return condition;
194
197
  if (condition == null) return defaults;
195
- condition = to_string(condition).trim() as ConditionExpression & string;
198
+ if (typeof condition == 'boolean') return condition;
196
199
  if (condition === '') return defaults;
197
200
  if (condition === 'true') return true;
198
201
  if (condition === 'false') return false;
199
202
  try {
200
- return this.evaluate(Expression(condition, 'boolean'), scope, defaults);
203
+ const result = this.evaluateSource(condition, scope, defaults);
204
+ if (typeof result == 'boolean') return result;
205
+ throw new Error(`Condition evaluates to \`${serialize(result)}\``);
201
206
  } catch (ex) {
202
207
  if (scope?.throws) {
203
208
  throw ex;
@@ -269,8 +274,10 @@ export class Evaluator {
269
274
  ): V {
270
275
  // 默认值有可能为表达式,因此需要先填充再求值
271
276
  expression ??= definition.value as ExpressionOrValue<V>;
277
+ // 求值失败时返回默认值
272
278
  const result = this.evaluate<V>(expression, scope, definition.value as V);
273
- return toArgumentValue<V>(result, definition);
279
+ // 转换失败时返回转换前的值
280
+ return toArgumentValue<V>(result, definition) ?? result;
274
281
  }
275
282
  /** 求参数表的所有参数值 */
276
283
  evaluateArgs<T extends Record<string, ArgumentValue>>(
@@ -6,7 +6,6 @@ import {
6
6
  isObjectNode,
7
7
  type FunctionNode,
8
8
  type MathNode,
9
- isOperatorNode,
10
9
  } from 'mathjs';
11
10
  import type { Options, Result } from './interface.js';
12
11
  import type { State } from './state.js';
@@ -14,6 +13,7 @@ import { constantValue, equalText, globalFnName, len, symbolName } from './utils
14
13
  import { migrateAtomic, migrateExpr, migrateNode } from './node.js';
15
14
  import { concat } from './concat.js';
16
15
  import { toNumber, toString } from './to-type.js';
16
+ import { isVmArray, isVmPrimitive } from '@mirascript/mirascript';
17
17
 
18
18
  const EXACT_UNARY_FUNCTION = new Map<string, Result['type']>([
19
19
  // 只支持标量的 mathjs 函数
@@ -71,6 +71,11 @@ function migrateFunctionCall(
71
71
  type: EXACT_UNARY_FUNCTION.get(fnName)!,
72
72
  code: `${fnName}(${arg.code})`,
73
73
  };
74
+ } else if (fnName === 'random' && args.length === 0) {
75
+ return {
76
+ type: 'number',
77
+ code: `${globalFnName(state, 'random')}()`,
78
+ };
74
79
  } else if (fnName === 're' || fnName === 'im') {
75
80
  const m = migrateAtomic(state, args[0]!);
76
81
  state.err(`不支持复数`);
@@ -264,26 +269,43 @@ function migrateFunctionCall(
264
269
  type: 'string',
265
270
  code: `${g('to_string')}(${inner.code})`,
266
271
  };
267
- } else if (
268
- fnName === 'sum' &&
269
- args.length === 1 &&
270
- isArrayNode(args[0]) &&
271
- args[0].items.length === 1 &&
272
- isOperatorNode(args[0].items[0]) &&
273
- ['>=', '>', '<=', '<', '==', '!='].includes(args[0].items[0].op) &&
274
- typeof constantValue(args[0].items[0].args[1]!) === 'number'
275
- ) {
276
- // sum([a > v])
277
- const a = migrateAtomic(state, args[0].items[0].args[0]!);
278
- const v = migrateAtomic(state, args[0].items[0].args[1]!);
272
+ } else if (fnName === 'sum' || fnName === 'prod') {
273
+ const newFnName = fnName === 'sum' ? 'sum' : 'product';
274
+ if (args.length !== 1) {
275
+ return {
276
+ type: 'number',
277
+ code: `${g(newFnName)}(${args.map((a) => migrateAtomic(state, a).code).join(', ')})`,
278
+ };
279
+ }
280
+ const arg = migrateAtomic(state, args[0]!);
281
+ if (arg.literal) {
282
+ if (isVmPrimitive(arg.literal)) {
283
+ return {
284
+ type: 'number',
285
+ code: `${g(newFnName)}(${arg.code})`,
286
+ };
287
+ }
288
+ if (isVmArray(arg.literal) && arg.literal.every((v) => isVmPrimitive(v))) {
289
+ return {
290
+ type: 'number',
291
+ code: `${g(newFnName)}(${arg.code})`,
292
+ };
293
+ }
294
+ }
295
+ if (arg.type === 'array' || !arg.type) {
296
+ return {
297
+ type: 'number',
298
+ code: `${g(newFnName)}(${arg.code}::${g('flatten')}())`,
299
+ };
300
+ }
279
301
  return {
280
302
  type: 'number',
281
- code: `${g('sum')}(${g('map')}(${a.code}, fn { it ${args[0].items[0].op} ${v.code} }))`,
303
+ code: `${g(newFnName)}(${arg.code})`,
282
304
  };
283
305
  }
284
306
 
285
307
  return call(state, node, (fn, args) => {
286
- const fun = state.globals.get(fnName);
308
+ const fun = state.globals.has(fnName) ? state.globals.get(fnName) : undefined;
287
309
  if (typeof fun === 'function') {
288
310
  if (args.some((a) => a.type !== 'number' && a.type !== 'boolean' && a.type !== 'string')) {
289
311
  state.warn(`函数行为可能不一致: ${fnName}`);
@@ -17,7 +17,7 @@ import {
17
17
  import type { State } from './state.js';
18
18
  import type { Options, Result } from './interface.js';
19
19
  import type { VmConst } from '@mirascript/mirascript';
20
- import { unsupportedNode } from './utils.js';
20
+ import { constantValue, unsupportedNode } from './utils.js';
21
21
  import { migrateAccess } from './access.js';
22
22
  import { migrateCall } from './call.js';
23
23
  import { migrateOperator } from './operator.js';
@@ -25,6 +25,7 @@ import { migrateSymbol } from './symbol.js';
25
25
  import { migrateCondition } from './condition.js';
26
26
  import { migrateFunctionAssignment } from './function.js';
27
27
  import { serialize } from './serialize.js';
28
+ import { toNumber } from './to-type.js';
28
29
 
29
30
  /** 转换 AST */
30
31
  export function migrateAtomic(state: State, node: MathNode): Result {
@@ -113,6 +114,26 @@ export function migrateNode(state: State, node: MathNode, options: Options): Res
113
114
  }
114
115
  if (isRelationalNode(node)) {
115
116
  const { conditionals, params } = node;
117
+ // 优化范围判断 a <= b <= c
118
+ if (
119
+ conditionals.length === 2 &&
120
+ params.length === 3 &&
121
+ conditionals[0] === 'smallerEq' &&
122
+ conditionals[1] === 'smallerEq'
123
+ ) {
124
+ // 模式匹配只支持常量
125
+ const l = constantValue(params[0]!);
126
+ const r = constantValue(params[2]!);
127
+ if (typeof l == 'number' && typeof r == 'number' && l < r) {
128
+ const v = toNumber(state, migrateNode(state, params[1]!, options));
129
+ let code = `${v.code} is ${l}..${r}`;
130
+ if (options.format !== 'no-paren') code = `(${code})`;
131
+ return {
132
+ type: 'boolean',
133
+ code,
134
+ };
135
+ }
136
+ }
116
137
  const exprs = [];
117
138
  for (let i = 0; i < conditionals.length; i++) {
118
139
  const fn = conditionals[i]! as OperatorNodeFn;
@@ -8,6 +8,7 @@ import {
8
8
  isOperatorNode,
9
9
  isNode,
10
10
  } from 'mathjs';
11
+ import { isVmArray, type TypeName, type VmAny } from '@mirascript/mirascript';
11
12
  import { operations } from '@mirascript/mirascript/subtle';
12
13
  import { symbolName, constantValue, equalText, scalar, globalFnName } from './utils.js';
13
14
  import type { State } from './state.js';
@@ -91,10 +92,12 @@ const BIT_OPS_TO_BOOL_OPS = {
91
92
  } as const;
92
93
 
93
94
  const COMPARE_OPERATORS = {
94
- smaller: '<',
95
- smallerEq: '<=',
96
- larger: '>',
97
- largerEq: '>=',
95
+ smaller: ['<'],
96
+ smallerEq: ['<='],
97
+ larger: ['>'],
98
+ largerEq: ['>='],
99
+ equal: ['=~', '=='],
100
+ unequal: ['!~', '!='],
98
101
  } as const;
99
102
 
100
103
  /** 转换为 boolean */
@@ -103,6 +106,21 @@ function b(op: string, state: State, node: MathNode): string {
103
106
  return toBoolean(state, scalar(op, state, re)).code;
104
107
  }
105
108
 
109
+ /** 数组元素类型 */
110
+ function elementType(lit: VmAny): TypeName | undefined {
111
+ if (!isVmArray(lit) || !lit.length) return undefined;
112
+ let type: TypeName | undefined = undefined;
113
+ for (const e of lit) {
114
+ const t = operations.$Type(e as VmAny);
115
+ if (!type) {
116
+ type = t;
117
+ } else if (type !== t) {
118
+ return undefined;
119
+ }
120
+ }
121
+ return type;
122
+ }
123
+
106
124
  /** 二元操作 */
107
125
  function binary(
108
126
  state: State,
@@ -115,23 +133,27 @@ function binary(
115
133
  return op(state, l, r);
116
134
  }
117
135
  if (Array.isArray(l.literal) && l.literal.every((e) => !Array.isArray(e))) {
136
+ const it = { code: 'it', type: elementType(l.literal) };
118
137
  return {
119
138
  type: 'array',
120
- code: `${l.code}::${globalFnName(state, 'map')}(fn { ${op(state, { code: 'it' }, r).code} })`,
139
+ code: `${l.code}::${globalFnName(state, 'map')}(fn { ${op(state, it, r).code} })`,
121
140
  };
122
141
  }
123
142
  if (Array.isArray(r.literal) && r.literal.every((e) => !Array.isArray(e))) {
143
+ const it = { code: 'it', type: elementType(r.literal) };
124
144
  return {
125
145
  type: 'array',
126
- code: `${r.code}::${globalFnName(state, 'map')}(fn { ${op(state, l, { code: 'it' }).code} })`,
146
+ code: `${r.code}::${globalFnName(state, 'map')}(fn { ${op(state, l, it).code} })`,
127
147
  };
128
148
  }
129
149
  if (alt) {
130
150
  return alt(state, l, r);
131
151
  }
152
+ const a = { code: 'a', type: l.type === 'array' ? elementType(l.literal) : l.type };
153
+ const b = { code: 'b', type: r.type === 'array' ? elementType(r.literal) : r.type };
132
154
  return {
133
155
  type: l.type === 'array' || r.type === 'array' ? 'array' : undefined,
134
- code: `${globalFnName(state, 'matrix')}.entrywise(${l.code}, ${r.code}, fn (a, b) { ${op(state, { code: 'a' }, { code: 'b' }).code} })`,
156
+ code: `${globalFnName(state, 'matrix')}.entrywise(${l.code}, ${r.code}, fn (a, b) { ${op(state, a, b).code} })`,
135
157
  };
136
158
  }
137
159
 
@@ -141,9 +163,10 @@ function unary(state: State, v: Result, op: (state: State, v: Result) => Result)
141
163
  return op(state, v);
142
164
  }
143
165
  if (Array.isArray(v.literal) && v.literal.every((e) => !Array.isArray(e))) {
166
+ const it = { code: 'it', type: elementType(v.literal) };
144
167
  return {
145
168
  type: 'array',
146
- code: `${v.code}::${globalFnName(state, 'map')}(fn { ${op(state, { code: 'it' }).code} })`,
169
+ code: `${v.code}::${globalFnName(state, 'map')}(fn { ${op(state, it).code} })`,
147
170
  };
148
171
  }
149
172
  return {
@@ -288,17 +311,21 @@ export function migrateOperator(
288
311
  }
289
312
 
290
313
  case 'equal':
291
- case 'unequal': {
292
- const op = fn === 'equal' ? '==' : '!=';
314
+ case 'unequal':
315
+ case 'smaller':
316
+ case 'smallerEq':
317
+ case 'larger':
318
+ case 'largerEq': {
319
+ const [op, eqOp] = COMPARE_OPERATORS[fn];
293
320
  const c0 = constantValue(a0);
294
321
  const c1 = constantValue(a1);
295
- if (c0 === null || c1 === null) {
296
- state.loose();
297
- const p0 = c0 === null ? { code: `nil` } : scalar(op, state, migrateExpr(state, a0));
298
- const p1 = c1 === null ? { code: `nil` } : scalar(op, state, migrateExpr(state, a1));
322
+ if (eqOp && (c0 === null || c1 === null)) {
323
+ // Mathjs 只支持标量与 null 比较
324
+ const p0 = c0 === null ? { code: `nil` } : migrateExpr(state, a0);
325
+ const p1 = c1 === null ? { code: `nil` } : migrateExpr(state, a1);
299
326
  return {
300
327
  type: 'boolean',
301
- code: `${open}${p0.code} ${op} ${p1.code}${close}`,
328
+ code: `${open}${p0.code} ${eqOp} ${p1.code}${close}`,
302
329
  };
303
330
  }
304
331
  // PI, E 与常量比较时,不进行转换
@@ -317,7 +344,19 @@ export function migrateOperator(
317
344
  };
318
345
 
319
346
  return binary(state, a(a0), a(a1), (state, l, r) => {
320
- if (l.type === 'boolean' && r.type === 'boolean') {
347
+ if (l.literal !== undefined) {
348
+ return {
349
+ type: 'boolean',
350
+ code: `${toNumber(state, l).code} ${op} ${r.code}`,
351
+ };
352
+ }
353
+ if (r.literal !== undefined) {
354
+ return {
355
+ type: 'boolean',
356
+ code: `${l.code} ${op} ${toNumber(state, r).code}`,
357
+ };
358
+ }
359
+ if (l.type === 'number' || r.type === 'number') {
321
360
  return {
322
361
  type: 'boolean',
323
362
  code: `${l.code} ${op} ${r.code}`,
@@ -329,16 +368,6 @@ export function migrateOperator(
329
368
  };
330
369
  });
331
370
  }
332
- case 'smaller':
333
- case 'smallerEq':
334
- case 'larger':
335
- case 'largerEq': {
336
- const op = COMPARE_OPERATORS[fn];
337
- return {
338
- type: 'boolean',
339
- code: `${open}${scalar(op, state, migrateExpr(state, a0)).code} ${op} ${scalar(op, state, migrateExpr(state, a1)).code}${close}`,
340
- };
341
- }
342
371
 
343
372
  default: {
344
373
  state.err(`不支持的运算符: ${fn}`);
@@ -14,8 +14,8 @@ export function migrateSymbol(state: State, node: SymbolNode, migrateConst: bool
14
14
  code: migrateSymbolName(name, true),
15
15
  };
16
16
  }
17
- const global = state.globals.get(name);
18
- if (global != null) {
17
+ if (state.globals.has(name)) {
18
+ const global = state.globals.get(name);
19
19
  return {
20
20
  type: operations.$Type(global),
21
21
  code: migrateSymbolName(name, false),
@@ -9,7 +9,7 @@ import { globalFnName } from './utils.js';
9
9
  export function toBoolean(state: State, node: MathNode | Result): Result {
10
10
  const helper = (): void => {
11
11
  state.helper(
12
- "fn @@to_boolean { if it is nil { return nil; } it != '' && it != '0' && it != 0 && it is not nan && !!it }",
12
+ "fn @@to_boolean { if it is nil { nil } else { it != '' && it != '0' && it != 0 && it is not nan && !!it } }",
13
13
  );
14
14
  };
15
15
  if (!isNode(node)) {
@@ -85,7 +85,7 @@ export function toNumber(state: State, node: MathNode | Result): Result {
85
85
  code: node.as_number,
86
86
  };
87
87
  }
88
- if (node.type === 'string' && node.literal !== undefined) {
88
+ if ((node.type === 'string' || node.type === 'boolean') && node.literal !== undefined) {
89
89
  const lit = Number(node.literal);
90
90
  return {
91
91
  type: 'number',
package/src/type.ts CHANGED
@@ -1,6 +1,5 @@
1
1
  import type { TypeName, VmValue, VmValueMap } from '@mirascript/mirascript';
2
- import { lib } from '@mirascript/mirascript/subtle';
3
- const { to_string, to_number, to_boolean } = lib;
2
+ import { convert } from '@mirascript/mirascript/subtle';
4
3
 
5
4
  /** 类型信息 */
6
5
  export type TypeInfo = 'string' | 'number' | 'boolean';
@@ -13,10 +12,11 @@ type TypeMap = VmValueMap & {
13
12
  s: string;
14
13
  f: number;
15
14
  b: boolean;
15
+ '': VmValue;
16
16
  };
17
17
 
18
18
  /** Ts 类型 */
19
- export type TsTypeOf<T extends TypeInfo | LegacyType> = TypeMap[T];
19
+ export type TsTypeOf<T extends TypeInfo | LegacyType | '', I extends VmValue = VmValue> = T extends '' ? I : TypeMap[T];
20
20
 
21
21
  /** 兼容的旧类型 */
22
22
  export type LegacyType = 's' | 'f' | 'b';
@@ -49,39 +49,48 @@ export namespace TypeInfo {
49
49
  }
50
50
 
51
51
  /** 转换类型 */
52
- export function to<T extends TypeInfo | LegacyType>(value: VmValue | undefined, type: T): TsTypeOf<T> {
53
- /** 返回类型 */
54
- type R = TsTypeOf<T>;
55
- value ??= null;
56
- if (!type) return value as R;
52
+ export function to<T extends TypeInfo | LegacyType | '', I extends VmValue = VmValue, F = undefined>(
53
+ value: I | undefined,
54
+ type: T,
55
+ fallback?: F,
56
+ ): TsTypeOf<T> | Exclude<F, undefined> {
57
+ const c = converter(type);
58
+ return (c as typeof convert.toNumber)(value, fallback) as TsTypeOf<T>;
59
+ }
60
+
61
+ /** 不转换 */
62
+ function identity<T extends VmValue>(value: T | undefined): T | null {
63
+ return value ?? null;
64
+ }
65
+
66
+ /** 获取转换器 */
67
+ type Converters = {
68
+ string: typeof convert.toString;
69
+ s: typeof convert.toString;
70
+ number: typeof convert.toNumber;
71
+ f: typeof convert.toNumber;
72
+ boolean: typeof convert.toBoolean;
73
+ b: typeof convert.toBoolean;
74
+ '': typeof identity;
75
+ };
76
+ /** 获取转换器 */
77
+ export function converter<T extends TypeInfo | LegacyType | ''>(type: T): Converters[T] {
78
+ // eslint-disable-next-line @typescript-eslint/switch-exhaustiveness-check
57
79
  switch (type) {
58
80
  case 'b':
59
81
  case 'boolean':
60
- return toBoolean(value) as R;
82
+ return toBoolean as Converters[T];
61
83
  case 'f':
62
84
  case 'number':
63
- return toNumber(value) as R;
85
+ return toNumber as Converters[T];
64
86
  case 's':
65
87
  case 'string':
66
- return toString(value) as R;
88
+ return toString as Converters[T];
67
89
  default:
68
- (type) satisfies never;
69
- return value as R;
90
+ (type) satisfies '';
91
+ return identity as Converters[T];
70
92
  }
71
93
  }
72
94
 
73
- /** 转换为字符串 */
74
- export function toString(value: VmValue | undefined): string {
75
- return to_string(value ?? null);
76
- }
77
-
78
- /** 转换为数字 */
79
- export function toNumber(value: VmValue | undefined): number {
80
- return to_number(value ?? null);
81
- }
82
-
83
- /** 转换为布尔值 */
84
- export function toBoolean(value: VmValue | undefined): boolean {
85
- return to_boolean(value ?? null);
86
- }
95
+ export const { toBoolean, toFormat, toNumber, toString } = convert;
87
96
  }
@@ -27,20 +27,28 @@ describe('Evaluate conditions', () => {
27
27
  expect(e.evaluateCondition('true' as ExpressionSource<boolean>, s)).toBe(true);
28
28
  expect(e.evaluateCondition('false' as ExpressionSource<boolean>, s)).toBe(false);
29
29
  });
30
- it('can convert to boolean', () => {
31
- expect(e.evaluateCondition('""' as ExpressionSource<boolean>, s)).toBe(true);
32
- expect(e.evaluateCondition('nil' as ExpressionSource<boolean>, s)).toBe(true);
33
- expect(e.evaluateCondition('undefined' as ExpressionSource<boolean>, s)).toBe(true);
34
- expect(e.evaluateCondition('0' as ExpressionSource<boolean>, s)).toBe(true);
35
- expect(e.evaluateCondition('nan' as ExpressionSource<boolean>, s)).toBe(true);
30
+ it('can evaluate to boolean', () => {
36
31
  expect(e.evaluateCondition('(false)' as ExpressionSource<boolean>, s)).toBe(false);
37
-
38
- expect(e.evaluateCondition('""' as ExpressionSource<boolean>, s, false)).toBe(true);
39
- expect(e.evaluateCondition('nil' as ExpressionSource<boolean>, s, false)).toBe(false);
40
- expect(e.evaluateCondition('undefined' as ExpressionSource<boolean>, s, false)).toBe(false);
41
- expect(e.evaluateCondition('0' as ExpressionSource<boolean>, s, false)).toBe(true);
42
- expect(e.evaluateCondition('nan' as ExpressionSource<boolean>, s, false)).toBe(true);
32
+ expect(e.evaluateCondition('(true)' as ExpressionSource<boolean>, s)).toBe(true);
43
33
  expect(e.evaluateCondition('(false)' as ExpressionSource<boolean>, s, false)).toBe(false);
34
+ expect(e.evaluateCondition('(true)' as ExpressionSource<boolean>, s, false)).toBe(true);
35
+ });
36
+ it('can evaluate to nil', () => {
37
+ expect(e.evaluateCondition('{}' as ExpressionSource<boolean>, s)).toBe(true);
38
+ expect(e.evaluateCondition('{}' as ExpressionSource<boolean>, s, false)).toBe(false);
39
+ });
40
+ it('do not convert to boolean', () => {
41
+ expect(en.evaluateCondition('""' as ExpressionSource<boolean>, s)).toBe(true);
42
+ expect(en.evaluateCondition('nil' as ExpressionSource<boolean>, s)).toBe(true);
43
+ expect(en.evaluateCondition('undefined' as ExpressionSource<boolean>, s)).toBe(true);
44
+ expect(en.evaluateCondition('0' as ExpressionSource<boolean>, s)).toBe(true);
45
+ expect(en.evaluateCondition('nan' as ExpressionSource<boolean>, s)).toBe(true);
46
+
47
+ expect(en.evaluateCondition('""' as ExpressionSource<boolean>, s, false)).toBe(false);
48
+ expect(en.evaluateCondition('nil' as ExpressionSource<boolean>, s, false)).toBe(false);
49
+ expect(en.evaluateCondition('undefined' as ExpressionSource<boolean>, s, false)).toBe(false);
50
+ expect(en.evaluateCondition('0' as ExpressionSource<boolean>, s, false)).toBe(false);
51
+ expect(en.evaluateCondition('nan' as ExpressionSource<boolean>, s, false)).toBe(false);
44
52
  });
45
53
  it('can evaluate complex', () => {
46
54
  expect(e.evaluateCondition('12>3' as ExpressionSource<boolean>, s)).toBe(true);