porffor 0.0.0-ebc0491 → 0.0.0-f74a73a

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.
@@ -5,6 +5,7 @@ import { BuiltinFuncs, BuiltinVars, importedFuncs, NULL, UNDEFINED } from "./bui
5
5
  import { PrototypeFuncs } from "./prototype.js";
6
6
  import { number, i32x4 } from "./embedding.js";
7
7
  import parse from "./parse.js";
8
+ import * as Rhemyn from "../rhemyn/compile.js";
8
9
 
9
10
  let globals = {};
10
11
  let globalInd = 0;
@@ -108,8 +109,8 @@ const generate = (scope, decl, global = false, name = undefined) => {
108
109
  case 'WhileStatement':
109
110
  return generateWhile(scope, decl);
110
111
 
111
- /* case 'ForOfStatement':
112
- return generateForOf(scope, decl); */
112
+ case 'ForOfStatement':
113
+ return generateForOf(scope, decl);
113
114
 
114
115
  case 'BreakStatement':
115
116
  return generateBreak(scope, decl);
@@ -731,21 +732,15 @@ const performOp = (scope, op, left, right, leftType, rightType, _global = false,
731
732
  ];
732
733
  };
733
734
 
734
- let binaryExpDepth = 0;
735
735
  const generateBinaryExp = (scope, decl, _global, _name) => {
736
- binaryExpDepth++;
737
-
738
- const out = [
739
- ...performOp(scope, decl.operator, generate(scope, decl.left), generate(scope, decl.right), getNodeType(scope, decl.left), getNodeType(scope, decl.right), _global, _name)
740
- ];
736
+ const out = performOp(scope, decl.operator, generate(scope, decl.left), generate(scope, decl.right), getNodeType(scope, decl.left), getNodeType(scope, decl.right), _global, _name);
741
737
 
742
738
  if (valtype !== 'i32' && ['==', '===', '!=', '!==', '>', '>=', '<', '<='].includes(decl.operator)) out.push(Opcodes.i32_from_u);
743
739
 
744
- binaryExpDepth--;
745
740
  return out;
746
741
  };
747
742
 
748
- const asmFunc = (name, { wasm, params, locals: localTypes, globals: globalTypes = [], globalInits, returns, returnType, memory, localNames = [], globalNames = [] }) => {
743
+ const asmFunc = (name, { wasm, params, locals: localTypes, globals: globalTypes = [], globalInits, returns, returnType, localNames = [], globalNames = [] }) => {
749
744
  const existing = funcs.find(x => x.name === name);
750
745
  if (existing) return existing;
751
746
 
@@ -781,7 +776,6 @@ const asmFunc = (name, { wasm, params, locals: localTypes, globals: globalTypes
781
776
  returns,
782
777
  returnType: TYPES[returnType ?? 'number'],
783
778
  wasm,
784
- memory,
785
779
  internal: true,
786
780
  index: currentFuncIndex++
787
781
  };
@@ -814,7 +808,8 @@ const TYPES = {
814
808
  bigint: 0xffffffffffff7,
815
809
 
816
810
  // these are not "typeof" types but tracked internally
817
- _array: 0xffffffffffff8
811
+ _array: 0xfffffffffff0f,
812
+ _regexp: 0xfffffffffff1f
818
813
  };
819
814
 
820
815
  const TYPE_NAMES = {
@@ -849,6 +844,8 @@ const getType = (scope, _name) => {
849
844
  const getNodeType = (scope, node) => {
850
845
  if (node.type === 'Literal') {
851
846
  if (['number', 'boolean', 'string', 'undefined', 'object', 'function', 'symbol', 'bigint'].includes(node.value)) return TYPES.number;
847
+ if (node.regex) return TYPES._regexp;
848
+
852
849
  return TYPES[typeof node.value];
853
850
  }
854
851
 
@@ -882,6 +879,11 @@ const getNodeType = (scope, node) => {
882
879
 
883
880
  // literal.func()
884
881
  if (!name && node.callee.type === 'MemberExpression') {
882
+ if (node.callee.object.regex) {
883
+ const funcName = node.callee.property.name;
884
+ return Rhemyn[funcName] ? TYPES.boolean : TYPES.undefined;
885
+ }
886
+
885
887
  const baseType = getNodeType(scope, node.callee.object);
886
888
 
887
889
  const func = node.callee.property.name;
@@ -925,6 +927,11 @@ const getNodeType = (scope, node) => {
925
927
  const generateLiteral = (scope, decl, global, name) => {
926
928
  if (decl.value === null) return number(NULL);
927
929
 
930
+ if (decl.regex) {
931
+ scope.regex[name] = decl.regex;
932
+ return number(1);
933
+ }
934
+
928
935
  switch (typeof decl.value) {
929
936
  case 'number':
930
937
  return number(decl.value);
@@ -1084,6 +1091,25 @@ const generateCall = (scope, decl, _global, _name) => {
1084
1091
 
1085
1092
  // literal.func()
1086
1093
  if (!name && decl.callee.type === 'MemberExpression') {
1094
+ // megahack for /regex/.func()
1095
+ if (decl.callee.object.regex) {
1096
+ const funcName = decl.callee.property.name;
1097
+ const func = Rhemyn[funcName](decl.callee.object.regex.pattern, currentFuncIndex++);
1098
+
1099
+ funcIndex[func.name] = func.index;
1100
+ funcs.push(func);
1101
+
1102
+ return [
1103
+ // make string arg
1104
+ ...generate(scope, decl.arguments[0]),
1105
+
1106
+ // call regex func
1107
+ Opcodes.i32_to_u,
1108
+ [ Opcodes.call, func.index ],
1109
+ Opcodes.i32_from
1110
+ ];
1111
+ }
1112
+
1087
1113
  baseType = getNodeType(scope, decl.callee.object);
1088
1114
 
1089
1115
  const func = decl.callee.property.name;
@@ -1096,6 +1122,31 @@ const generateCall = (scope, decl, _global, _name) => {
1096
1122
  baseName = [...arrays.keys()].pop();
1097
1123
  }
1098
1124
 
1125
+ if (protoName && baseType === TYPES.string && Rhemyn[protoName]) {
1126
+ const func = Rhemyn[protoName](decl.arguments[0].regex.pattern, currentFuncIndex++);
1127
+
1128
+ funcIndex[func.name] = func.index;
1129
+ funcs.push(func);
1130
+
1131
+ const pointer = arrays.get(baseName);
1132
+ const [ local, isGlobal ] = lookupName(scope, baseName);
1133
+
1134
+ return [
1135
+ ...out,
1136
+
1137
+ ...(pointer == null ? [
1138
+ [ isGlobal ? Opcodes.global_get : Opcodes.local_get, local.idx ],
1139
+ Opcodes.i32_to_u,
1140
+ ] : [
1141
+ ...number(pointer, Valtype.i32)
1142
+ ]),
1143
+
1144
+ // call regex func
1145
+ [ Opcodes.call, func.index ],
1146
+ Opcodes.i32_from
1147
+ ];
1148
+ }
1149
+
1099
1150
  if (protoFunc) {
1100
1151
  let pointer = arrays.get(baseName);
1101
1152
 
@@ -1510,7 +1561,7 @@ const generateUpdate = (scope, decl) => {
1510
1561
  };
1511
1562
 
1512
1563
  const generateIf = (scope, decl) => {
1513
- const out = truthy(scope, generate(scope, decl.test), decl.test);
1564
+ const out = truthy(scope, generate(scope, decl.test), getNodeType(scope, decl.test));
1514
1565
 
1515
1566
  out.push(Opcodes.i32_to, [ Opcodes.if, Blocktype.void ]);
1516
1567
  depth.push('if');
@@ -1603,18 +1654,106 @@ const generateWhile = (scope, decl) => {
1603
1654
  const generateForOf = (scope, decl) => {
1604
1655
  const out = [];
1605
1656
 
1657
+ const rightType = getNodeType(scope, decl.right);
1658
+ const valtypeSize = rightType === TYPES._array ? ValtypeSize[valtype] : ValtypeSize.i16; // presume array (:()
1659
+
1660
+ // todo: for of inside for of might fuck up?
1661
+ const pointer = localTmp(scope, 'forof_base_pointer', Valtype.i32);
1662
+ const length = localTmp(scope, 'forof_length', Valtype.i32);
1663
+ const counter = localTmp(scope, 'forof_counter', Valtype.i32);
1664
+
1665
+ out.push(
1666
+ // set pointer as right
1667
+ ...generate(scope, decl.right),
1668
+ Opcodes.i32_to_u,
1669
+ [ Opcodes.local_set, pointer ],
1670
+
1671
+ // get length
1672
+ [ Opcodes.local_get, pointer ],
1673
+ [ Opcodes.i32_load, Math.log2(ValtypeSize.i32) - 1, 0 ],
1674
+ [ Opcodes.local_set, length ]
1675
+ );
1676
+
1606
1677
  out.push([ Opcodes.loop, Blocktype.void ]);
1607
- depth.push('while');
1678
+ depth.push('forof');
1608
1679
 
1609
- out.push(...generate(scope, decl.test));
1610
- out.push(Opcodes.i32_to, [ Opcodes.if, Blocktype.void ]);
1611
- depth.push('if');
1680
+ // setup local for left
1681
+ generate(scope, decl.left);
1612
1682
 
1613
- out.push(...generate(scope, decl.body));
1683
+ const leftName = decl.left.declarations[0].id.name;
1614
1684
 
1615
- out.push([ Opcodes.br, 1 ]);
1616
- out.push([ Opcodes.end ], [ Opcodes.end ]);
1617
- depth.pop(); depth.pop();
1685
+ // set type for local
1686
+ typeStates[leftName] = rightType === TYPES._array ? TYPES.number : TYPES.string;
1687
+
1688
+ const [ local, isGlobal ] = lookupName(scope, leftName);
1689
+
1690
+ if (rightType === TYPES._array) { // array
1691
+ out.push(
1692
+ [ Opcodes.local_get, pointer ],
1693
+ [ Opcodes.load, Math.log2(valtypeSize) - 1, ...unsignedLEB128(ValtypeSize.i32) ]
1694
+ );
1695
+ } else { // string
1696
+ const [ newOut, newPointer ] = makeArray(scope, {
1697
+ rawElements: new Array(1)
1698
+ }, isGlobal, leftName, true, 'i16');
1699
+
1700
+ out.push(
1701
+ // setup new/out array
1702
+ ...newOut,
1703
+ [ Opcodes.drop ],
1704
+
1705
+ ...number(0, Valtype.i32), // base 0 for store after
1706
+
1707
+ // load current string ind {arg}
1708
+ [ Opcodes.local_get, pointer ],
1709
+ [ Opcodes.i32_load16_u, Math.log2(ValtypeSize.i16) - 1, ...unsignedLEB128(ValtypeSize.i32) ],
1710
+
1711
+ // store to new string ind 0
1712
+ [ Opcodes.i32_store16, Math.log2(ValtypeSize.i16) - 1, ...unsignedLEB128(newPointer + ValtypeSize.i32) ],
1713
+
1714
+ // return new string (page)
1715
+ ...number(newPointer)
1716
+ );
1717
+ }
1718
+
1719
+ // set left value
1720
+ out.push([ isGlobal ? Opcodes.global_set : Opcodes.local_set, local.idx ]);
1721
+
1722
+ out.push(
1723
+ [ Opcodes.block, Blocktype.void ],
1724
+ [ Opcodes.block, Blocktype.void ]
1725
+ );
1726
+ depth.push('block');
1727
+ depth.push('block');
1728
+
1729
+ out.push(
1730
+ ...generate(scope, decl.body),
1731
+ [ Opcodes.end ]
1732
+ );
1733
+ depth.pop();
1734
+
1735
+ out.push(
1736
+ // increment iter pointer by valtype size
1737
+ [ Opcodes.local_get, pointer ],
1738
+ ...number(valtypeSize, Valtype.i32),
1739
+ [ Opcodes.i32_add ],
1740
+ [ Opcodes.local_set, pointer ],
1741
+
1742
+ // increment counter by 1
1743
+ [ Opcodes.local_get, counter ],
1744
+ ...number(1, Valtype.i32),
1745
+ [ Opcodes.i32_add ],
1746
+ [ Opcodes.local_tee, counter ],
1747
+
1748
+ // loop if counter != length
1749
+ [ Opcodes.local_get, length ],
1750
+ [ Opcodes.i32_ne ],
1751
+ [ Opcodes.br_if, 1 ],
1752
+
1753
+ [ Opcodes.end ], [ Opcodes.end ]
1754
+ );
1755
+ depth.pop();
1756
+ depth.pop();
1618
1757
 
1619
1758
  return out;
1620
1759
  };
@@ -1705,19 +1844,19 @@ const generateAssignPat = (scope, decl) => {
1705
1844
  };
1706
1845
 
1707
1846
  let pages = new Map();
1708
- const allocPage = reason => {
1709
- if (pages.has(reason)) return pages.get(reason);
1847
+ const allocPage = (reason, type) => {
1848
+ if (pages.has(reason)) return pages.get(reason).ind;
1710
1849
 
1711
- let ind = pages.size;
1712
- pages.set(reason, ind);
1850
+ const ind = pages.size;
1851
+ pages.set(reason, { ind, type });
1713
1852
 
1714
- if (allocLog) log('alloc', `allocated new page of memory (${ind}) | ${reason}`);
1853
+ if (allocLog) log('alloc', `allocated new page of memory (${ind}) | ${reason} (type: ${type})`);
1715
1854
 
1716
1855
  return ind;
1717
1856
  };
1718
1857
 
1719
1858
  const freePage = reason => {
1720
- let ind = pages.get(reason);
1859
+ const { ind } = pages.get(reason);
1721
1860
  pages.delete(reason);
1722
1861
 
1723
1862
  if (allocLog) log('alloc', `freed page of memory (${ind}) | ${reason}`);
@@ -1749,7 +1888,7 @@ const makeArray = (scope, decl, global = false, name = '$undeclared', initEmpty
1749
1888
  if (!arrays.has(name) || name === '$undeclared') {
1750
1889
  // todo: can we just have 1 undeclared array? probably not? but this is not really memory efficient
1751
1890
  const uniqueName = name === '$undeclared' ? name + Math.random().toString().slice(2) : name;
1752
- arrays.set(name, allocPage(`${itemType === 'i16' ? 'string' : 'array'}: ${uniqueName}`) * pageSize);
1891
+ arrays.set(name, allocPage(`${itemType === 'i16' ? 'string' : 'array'}: ${uniqueName}`, itemType) * pageSize);
1753
1892
  }
1754
1893
 
1755
1894
  const pointer = arrays.get(name);
@@ -1929,7 +2068,6 @@ const generateFunc = (scope, decl) => {
1929
2068
  localInd: 0,
1930
2069
  returns: [ valtypeBinary ],
1931
2070
  returnType: null,
1932
- memory: false,
1933
2071
  throws: false,
1934
2072
  name
1935
2073
  };
@@ -57,7 +57,7 @@ export default (wasm, name = '', ind = 0, locals = {}, params = [], returns = []
57
57
  out += ` ;; label @${depth}`;
58
58
  }
59
59
 
60
- if (inst[0] === Opcodes.br) {
60
+ if (inst[0] === Opcodes.br || inst[0] === Opcodes.br_if) {
61
61
  out += ` ;; goto @${depth - inst[1]}`;
62
62
  }
63
63
 
package/compiler/index.js CHANGED
@@ -4,6 +4,8 @@ import opt from './opt.js';
4
4
  import produceSections from './sections.js';
5
5
  import decompile from './decompile.js';
6
6
  import { BuiltinPreludes } from './builtins.js';
7
+ import toc from './2c.js';
8
+
7
9
 
8
10
  globalThis.decompile = decompile;
9
11
 
@@ -15,7 +17,8 @@ const areaColors = {
15
17
  codegen: [ 20, 80, 250 ],
16
18
  opt: [ 250, 20, 80 ],
17
19
  sections: [ 20, 250, 80 ],
18
- alloc: [ 250, 250, 20 ]
20
+ alloc: [ 250, 250, 20 ],
21
+ '2c': [ 20, 250, 250 ]
19
22
  };
20
23
 
21
24
  globalThis.log = (area, ...args) => console.log(`\u001b[90m[\u001b[0m${rgb(...areaColors[area], area)}\u001b[90m]\u001b[0m`, ...args);
@@ -36,10 +39,16 @@ const logFuncs = (funcs, globals, exceptions) => {
36
39
  console.log();
37
40
  };
38
41
 
42
+ const getArg = name => process.argv.find(x => x.startsWith(`-${name}=`))?.slice(name.length + 2);
43
+
44
+ const writeFileSync = (typeof process !== 'undefined' ? (await import('node:fs')).writeFileSync : undefined);
45
+ const execSync = (typeof process !== 'undefined' ? (await import('node:child_process')).execSync : undefined);
46
+
39
47
  export default (code, flags) => {
40
48
  globalThis.optLog = process.argv.includes('-opt-log');
41
49
  globalThis.codeLog = process.argv.includes('-code-log');
42
50
  globalThis.allocLog = process.argv.includes('-alloc-log');
51
+ globalThis.regexLog = process.argv.includes('-regex-log');
43
52
 
44
53
  for (const x in BuiltinPreludes) {
45
54
  if (code.indexOf(x + '(') !== -1) code = BuiltinPreludes[x] + code;
@@ -72,5 +81,38 @@ export default (code, flags) => {
72
81
  // console.log([...pages.keys()].map(x => `\x1B[36m - ${x}\x1B[0m`).join('\n'));
73
82
  }
74
83
 
75
- return { wasm: sections, funcs, globals, tags, exceptions, pages };
84
+ const out = { wasm: sections, funcs, globals, tags, exceptions, pages };
85
+
86
+ const target = getArg('target') ?? getArg('t') ?? 'wasm';
87
+ const outFile = getArg('o');
88
+
89
+ if (target === 'c') {
90
+ const c = toc(out);
91
+
92
+ if (outFile) {
93
+ writeFileSync(outFile, c);
94
+ } else {
95
+ console.log(c);
96
+ }
97
+
98
+ process.exit();
99
+ }
100
+
101
+ if (target === 'native') {
102
+ const compiler = getArg('compiler') ?? 'clang';
103
+ const cO = getArg('cO') ?? 'Ofast';
104
+
105
+ const tmpfile = 'tmp.c';
106
+ const args = [ compiler, tmpfile, '-o', outFile ?? (process.platform === 'win32' ? 'out.exe' : 'out'), '-' + cO, '-march=native' ];
107
+
108
+ const c = toc(out);
109
+ writeFileSync(tmpfile, c);
110
+
111
+ // obvious command escape is obvious
112
+ execSync(args.join(' '), { stdio: 'inherit' });
113
+
114
+ process.exit();
115
+ }
116
+
117
+ return out;
76
118
  };
package/compiler/opt.js CHANGED
@@ -142,7 +142,7 @@ export default (funcs, globals) => {
142
142
  depth--;
143
143
  if (depth <= 0) break;
144
144
  }
145
- if (op === Opcodes.br) {
145
+ if (op === Opcodes.br || op === Opcodes.br_if) {
146
146
  hasBranch = true;
147
147
  break;
148
148
  }
package/compiler/parse.js CHANGED
@@ -1,3 +1,4 @@
1
+ // import { parse } from 'acorn';
1
2
  const { parse } = (await import(globalThis.document ? 'https://esm.sh/acorn' : 'acorn'));
2
3
 
3
4
  export default (input, flags) => {
@@ -15,7 +15,8 @@ const TYPES = {
15
15
  bigint: 0xffffffffffff7,
16
16
 
17
17
  // these are not "typeof" types but tracked internally
18
- _array: 0xffffffffffff8
18
+ _array: 0xfffffffffff0f,
19
+ _regexp: 0xfffffffffff1f
19
20
  };
20
21
 
21
22
  // todo: turn these into built-ins once arrays and these become less hacky
@@ -76,6 +76,7 @@ export default (funcs, globals, tags, pages, flags) => {
76
76
  }
77
77
  }
78
78
  }
79
+ globalThis.importFuncs = importFuncs;
79
80
 
80
81
  if (optLog) log('sections', `treeshake: using ${importFuncs.length}/${importedFuncs.length} imports`);
81
82
 
package/compiler/wrap.js CHANGED
@@ -1,10 +1,11 @@
1
1
  import compile from './index.js';
2
2
  import decompile from './decompile.js';
3
- import fs from 'node:fs';
3
+ // import fs from 'node:fs';
4
4
 
5
5
  const bold = x => `\u001b[1m${x}\u001b[0m`;
6
6
 
7
7
  const typeBase = 0xffffffffffff0;
8
+ const internalTypeBase = 0xfffffffffff0f;
8
9
  const TYPES = {
9
10
  [typeBase]: 'number',
10
11
  [typeBase + 1]: 'boolean',
@@ -16,7 +17,8 @@ const TYPES = {
16
17
  [typeBase + 7]: 'bigint',
17
18
 
18
19
  // internal
19
- [typeBase + 8]: '_array'
20
+ [internalTypeBase]: '_array',
21
+ [internalTypeBase + 1]: '_regexp'
20
22
  };
21
23
 
22
24
  export default async (source, flags = [ 'module' ], customImports = {}, print = str => process.stdout.write(str)) => {
@@ -27,7 +29,7 @@ export default async (source, flags = [ 'module' ], customImports = {}, print =
27
29
 
28
30
  if (source.includes('export function')) flags.push('module');
29
31
 
30
- fs.writeFileSync('out.wasm', Buffer.from(wasm));
32
+ // fs.writeFileSync('out.wasm', Buffer.from(wasm));
31
33
 
32
34
  times.push(performance.now() - t1);
33
35
  if (flags.includes('info')) console.log(bold(`compiled in ${times[0].toFixed(2)}ms`));
package/cool.exe ADDED
Binary file
package/g ADDED
Binary file
package/g.exe ADDED
Binary file
package/hi.c ADDED
@@ -0,0 +1,37 @@
1
+ #include <stdio.h>
2
+
3
+ double inline f64_f(double x, double y) {
4
+ return x - (int)(x / y) * y;
5
+ }
6
+
7
+ double isPrime(double number) {
8
+ double i;
9
+
10
+ if (number < 2e+0) {
11
+ return 0e+0;
12
+ }
13
+ i = 2e+0;
14
+ while (i < number) {
15
+ if (f64_f(number, i) == 0e+0) {
16
+ return 0e+0;
17
+ }
18
+ i = i + 1e+0;
19
+ }
20
+ return 1e+0;
21
+ }
22
+
23
+ int main() {
24
+ double sum;
25
+ double counter;
26
+
27
+ sum = 0e+0;
28
+ counter = 0e+0;
29
+ while (counter <= 1e+5) {
30
+ if (isPrime(counter) == 1e+0) {
31
+ sum = sum + counter;
32
+ }
33
+ counter = counter + 1e+0;
34
+ }
35
+ printf("%f\n", sum);
36
+ }
37
+
package/out ADDED
Binary file
package/out.exe ADDED
Binary file
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "porffor",
3
3
  "description": "a basic experimental wip aot optimizing js -> wasm engine/compiler/runtime in js",
4
- "version": "0.0.0-ebc0491",
4
+ "version": "0.0.0-f74a73a",
5
5
  "author": "CanadaHonk",
6
6
  "license": "MIT",
7
7
  "dependencies": {
package/r.js CHANGED
@@ -1 +1 @@
1
- /a(b)/.test('hi');
1
+ print(performance.now());
@@ -0,0 +1,37 @@
1
+ # rhemyn
2
+ a basic experimental wip regex engine/aot wasm compiler in js. regex engine for porffor. uses own regex parser, no dependencies (excluding porffor internals). <br>
3
+ age: ~1 day
4
+
5
+ made for use with porffor but could possibly be adapted, implementation/library notes:
6
+ - exposes functions for each regex "operation" (eg test, match)
7
+ - given a regex pattern string (eg `a+`), it returns a "function" object
8
+ - wasm function returned expects an i32 pointer to a utf-16 string (can add utf-8 option later if someone else actually wants to use this)
9
+
10
+ ## syntax
11
+ 🟢 supported 🟡 partial 🟠 parsed only 🔴 unsupported
12
+
13
+ - 🟢 literal characters (eg `a`)
14
+ - 🟢 escaping (eg `\.\n\cJ\x0a\u000a`)
15
+ - 🟢 character itself (eg `\.`)
16
+ - 🟢 escape sequences (eg `\n`)
17
+ - 🟢 control character (eg `\cJ`)
18
+ - 🟢 unicode code points (eg `\x00`, `\u0000`)
19
+ - 🟢 sets (eg `[ab]`)
20
+ - 🟢 ranges (eg `[a-z]`)
21
+ - 🟢 negated sets (eg `[^ab]`)
22
+ - 🟢 metacharacters
23
+ - 🟢 dot (eg `a.b`)
24
+ - 🟢 digit, not digit (eg `\d\D`)
25
+ - 🟢 word, not word (eg `\w\W`)
26
+ - 🟢 whitespace, not whitespace (eg `\s\S`)
27
+ - 🟠 quantifiers
28
+ - 🟠 star (eg `a*`)
29
+ - 🟠 plus (eg `a+`)
30
+ - 🟠 optional (eg `a?`)
31
+ - 🟠 lazy modifier (eg `a*?`)
32
+ - 🔴 n repetitions (eg `a{4}`)
33
+ - 🔴 n-m repetitions (eg `a{2,4}`)
34
+ - 🔴 assertions
35
+ - 🔴 beginning (eg `^a`)
36
+ - 🔴 end (eg `a$`)
37
+ - 🔴 word boundary assertion (eg `\b\B`)