@wrnrlr/prelude 0.0.1 → 0.1.2
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.
- package/.github/workflows/publish.yml +46 -5
- package/deno.json +18 -9
- package/package.json +4 -6
- package/readme.md +163 -41
- package/src/constants.ts +2 -1
- package/src/{controlflow.js → controlflow.ts} +63 -50
- package/src/hyperscript.ts +7 -6
- package/src/mod.ts +19 -17
- package/src/reactive.ts +42 -14
- package/src/resource.js +184 -0
- package/src/router.js +65 -0
- package/src/runtime.ts +9 -8
- package/test/hyperscript.js +2 -2
- package/test/reactive.js +12 -4
- package/www/assets/css/presets.css +504 -0
- package/www/assets/css/style.css +90 -0
- package/www/assets/fonts/fab.ttf +0 -0
- package/www/assets/fonts/fab.woff2 +0 -0
- package/www/assets/fonts/far.ttf +0 -0
- package/www/assets/fonts/far.woff2 +0 -0
- package/www/assets/fonts/fas.ttf +0 -0
- package/www/assets/fonts/fas.woff2 +0 -0
- package/www/index.html +211 -0
- package/www/playground.html +183 -0
- package/www/public/example/admin.html +88 -0
- package/{example → www/public/example}/counter.html +1 -1
- package/{example → www/public/example}/greeting.html +1 -1
- package/{example → www/public/example}/show.html +1 -1
- package/{example → www/public/example}/todo.html +1 -1
- package/www/public/logo.svg +16 -0
- package/www/typedoc.json +13 -0
- package/www/vite.config.js +106 -0
- package/example/paint.html +0 -22
- package/index.html +0 -230
- package/presets.css +0 -284
- package/public/logo.svg +0 -5
- package/test/runtime.js +0 -7
- package/typedoc.jsonc +0 -31
- /package/{public → www/public}/banner.svg +0 -0
package/src/reactive.ts
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
|
1
|
+
// @ts-nocheck:
|
2
2
|
export interface EffectOptions {
|
3
3
|
name?: string;
|
4
4
|
}
|
@@ -145,7 +145,7 @@ abstract class Observer {
|
|
145
145
|
|
146
146
|
class Root extends Observer {
|
147
147
|
wrap<T>(fn: RootFn<T>): T {
|
148
|
-
return
|
148
|
+
return observe(() => fn(this.dispose), this, false)!
|
149
149
|
}
|
150
150
|
}
|
151
151
|
|
@@ -165,7 +165,7 @@ class Computation<T = unknown> extends Observer {
|
|
165
165
|
private run = (): T => {
|
166
166
|
this.dispose()
|
167
167
|
this.parent?.observers.add(this)
|
168
|
-
return
|
168
|
+
return observe(this.fn, this, true)!
|
169
169
|
}
|
170
170
|
|
171
171
|
update = (): void => {
|
@@ -279,21 +279,52 @@ export function useContext<T>(context: Context<T>): T {
|
|
279
279
|
return context.get()
|
280
280
|
}
|
281
281
|
|
282
|
+
export type S<T> = Getter<T> | Setter<T>
|
283
|
+
|
284
|
+
/**
|
285
|
+
|
286
|
+
@param s Signal
|
287
|
+
@param k
|
288
|
+
*/
|
289
|
+
export function wrap<T>(s:S<Array<T>>, k:number|(()=>number)): S<T>
|
290
|
+
export function wrap<T>(s:S<Record<string,T>>, k:string|(()=>string)): S<T>
|
291
|
+
export function wrap<T>(s:S<Array<T>>|S<Record<string,T>>, k:number|string|(()=>number)|(()=>string)): S<T> {
|
292
|
+
const t = typeof k
|
293
|
+
if (t === 'number') {
|
294
|
+
return ((...a:T[]) => {
|
295
|
+
const b = (s as Getter<Array<T>>)()
|
296
|
+
return (a.length) ? (s as Setter<Array<T>>)((b as any).toSpliced(k as number, 1, a[0])).at(k as number) : b.at(k as number)
|
297
|
+
}) as S<T>
|
298
|
+
} else if (t === 'string') {
|
299
|
+
return ((...a:T[]) => {
|
300
|
+
const b = (s as Getter<Record<string,T>>)()
|
301
|
+
return (a.length) ? (s as Setter<Record<string,T>>)({...b, [k as string]:a[0]})[k as string] : b[k as string]
|
302
|
+
}) as S<T>
|
303
|
+
} else if (t === 'function')
|
304
|
+
return ((...a:T[]) => {
|
305
|
+
const i = (k as ()=>string|number)(), c = typeof i
|
306
|
+
if (c==='number') return a.length ? (s as Setter<Array<T>>)((old:any) => old.toSpliced(i, 1, a[0]))[i as number] : (s as Getter<Array<T>>)()[i as number]
|
307
|
+
else if (c === 'string') return a.length ? (s as Setter<Record<string,T>>)((b) => ({...b, [i]:a[0]}))[i as string] : (s as Getter<Record<string,T>>)()[i]
|
308
|
+
throw new Error('Cannot wrap signal')
|
309
|
+
}) as S<T>
|
310
|
+
throw new Error('Cannot wrap signal')
|
311
|
+
}
|
312
|
+
|
282
313
|
export function getOwner(): Observer | undefined {
|
283
314
|
return OBSERVER
|
284
315
|
}
|
285
316
|
|
286
317
|
export function runWithOwner<T>(observer: Observer|undefined, fn: ()=>T):T {
|
287
318
|
const tracking = observer instanceof Computation
|
288
|
-
return
|
319
|
+
return observe(fn, observer, tracking)!
|
289
320
|
}
|
290
321
|
|
291
322
|
/**
|
292
|
-
Execute the function `fn` only once. Implemented as an {@link effect} wrapping a {@link
|
323
|
+
Execute the function `fn` only once. Implemented as an {@link effect} wrapping a {@link untrack}.
|
293
324
|
@group Reactive Primitive
|
294
325
|
*/
|
295
326
|
export function onMount(fn: () => void) {
|
296
|
-
effect(() =>
|
327
|
+
effect(() => untrack(fn));
|
297
328
|
}
|
298
329
|
|
299
330
|
export function onCleanup(fn: Fn):void {
|
@@ -327,20 +358,17 @@ export function batch<T>(fn: Fn<T>):T {
|
|
327
358
|
}
|
328
359
|
|
329
360
|
/**
|
361
|
+
Get the value of a signal without subscribing to future updates.
|
330
362
|
|
331
363
|
@param fn
|
364
|
+
@returns value returned from `fn`
|
332
365
|
@group Reactive Primitive
|
333
366
|
*/
|
334
|
-
export function
|
335
|
-
return
|
367
|
+
export function untrack<T>(fn: ()=>T):T {
|
368
|
+
return observe(fn, OBSERVER, false)!
|
336
369
|
}
|
337
370
|
|
338
|
-
|
339
|
-
|
340
|
-
@param fn
|
341
|
-
@group Reactive Primitive
|
342
|
-
*/
|
343
|
-
function wrap<T>(fn: Fn<T>, observer: Observer | undefined, tracking: boolean ): T|undefined {
|
371
|
+
function observe<T>(fn: Fn<T>, observer: Observer | undefined, tracking: boolean ): T|undefined {
|
344
372
|
const OBSERVER_PREV = OBSERVER;
|
345
373
|
const TRACKING_PREV = TRACKING;
|
346
374
|
OBSERVER = observer;
|
package/src/resource.js
ADDED
@@ -0,0 +1,184 @@
|
|
1
|
+
// @ts-nocheck:
|
2
|
+
import {signal,effect,untrack,memo,batch,useContext,onCleanup} from './reactive.ts'
|
3
|
+
|
4
|
+
const NO_INIT = {}
|
5
|
+
|
6
|
+
/**
|
7
|
+
resource
|
8
|
+
*/
|
9
|
+
export function resource(pSource,pFetcher,pOptions) {
|
10
|
+
let source
|
11
|
+
let fetcher
|
12
|
+
let options
|
13
|
+
|
14
|
+
if ((arguments.length === 2 && typeof pFetcher === 'object') || arguments.length === 1) {
|
15
|
+
source = true
|
16
|
+
fetcher = pSource
|
17
|
+
options = (pFetcher || {})
|
18
|
+
} else {
|
19
|
+
source = pSource
|
20
|
+
fetcher = pFetcher
|
21
|
+
options = pOptions || {}
|
22
|
+
}
|
23
|
+
|
24
|
+
let pr = null,
|
25
|
+
initP = NO_INIT,
|
26
|
+
scheduled = false,
|
27
|
+
resolved = 'initialValue' in options
|
28
|
+
const dynamic = source.call && memo(source)
|
29
|
+
|
30
|
+
const contexts = new Set(),
|
31
|
+
value = signal(options.initialValue),
|
32
|
+
error = signal(undefined),
|
33
|
+
track = signal(undefined, {equals: false}),
|
34
|
+
state = signal(resolved ? 'ready' : 'unresolved')
|
35
|
+
|
36
|
+
function loadEnd(p, v, error, key) {
|
37
|
+
if (pr === p) {
|
38
|
+
pr = null
|
39
|
+
key !== undefined && (resolved = true)
|
40
|
+
initP = NO_INIT
|
41
|
+
completeLoad(v, error)
|
42
|
+
}
|
43
|
+
return v
|
44
|
+
}
|
45
|
+
|
46
|
+
function completeLoad(v, err) {
|
47
|
+
batch(() => {
|
48
|
+
if (err === undefined) value(() => v)
|
49
|
+
state(err !== undefined ? 'errored' : resolved ? 'ready' : 'unresolved')
|
50
|
+
error(err)
|
51
|
+
for (const c of contexts.keys()) c.decrement()
|
52
|
+
contexts.clear()
|
53
|
+
}, false)
|
54
|
+
}
|
55
|
+
|
56
|
+
function read() {
|
57
|
+
const v = value(),
|
58
|
+
err = error();
|
59
|
+
if (err !== undefined && !pr) throw err;
|
60
|
+
return v;
|
61
|
+
}
|
62
|
+
|
63
|
+
function load(refetching = true) {
|
64
|
+
if (refetching !== false && scheduled) return;
|
65
|
+
scheduled = false;
|
66
|
+
const lookup = dynamic ? dynamic() : (source);
|
67
|
+
|
68
|
+
if (lookup == null || lookup === false) {
|
69
|
+
loadEnd(pr, untrack(value));
|
70
|
+
return;
|
71
|
+
}
|
72
|
+
const p = initP !== NO_INIT ? (initP)
|
73
|
+
: untrack(() => fetcher(lookup, {value: value(), refetching}))
|
74
|
+
|
75
|
+
if (!isPromise(p)) {
|
76
|
+
loadEnd(pr, p, undefined, lookup);
|
77
|
+
return p
|
78
|
+
}
|
79
|
+
pr = p;
|
80
|
+
|
81
|
+
if ('value' in p) {
|
82
|
+
if ((p).status === "success") loadEnd(pr, p.value, undefined, lookup);
|
83
|
+
else loadEnd(pr, undefined, undefined, lookup);
|
84
|
+
return p;
|
85
|
+
}
|
86
|
+
|
87
|
+
scheduled = true
|
88
|
+
queueMicrotask(() => (scheduled = false));
|
89
|
+
batch(() => {
|
90
|
+
state(resolved ? 'refreshing' : 'pending')
|
91
|
+
track()
|
92
|
+
}, false);
|
93
|
+
return p.then(
|
94
|
+
v => loadEnd(p, v, undefined, lookup),
|
95
|
+
e => loadEnd(p, undefined, castError(e), lookup)
|
96
|
+
);
|
97
|
+
}
|
98
|
+
|
99
|
+
Object.defineProperties(read, {
|
100
|
+
state: { get: () => state() },
|
101
|
+
error: { get: () => error() },
|
102
|
+
loading: {
|
103
|
+
get() {
|
104
|
+
const s = state();
|
105
|
+
return s === 'pending' || s === 'refreshing';
|
106
|
+
}
|
107
|
+
},
|
108
|
+
latest: {
|
109
|
+
get() {
|
110
|
+
if (!resolved) return read();
|
111
|
+
const err = error();
|
112
|
+
if (err && !pr) throw err;
|
113
|
+
return value();
|
114
|
+
}
|
115
|
+
}
|
116
|
+
})
|
117
|
+
|
118
|
+
if (dynamic) effect(() => load(false));
|
119
|
+
else load(false);
|
120
|
+
|
121
|
+
return read
|
122
|
+
}
|
123
|
+
|
124
|
+
|
125
|
+
function isPromise(v) {
|
126
|
+
return v && typeof v === 'object' && 'then' in v
|
127
|
+
}
|
128
|
+
|
129
|
+
/**
|
130
|
+
Creates and handles an AbortSignal**
|
131
|
+
```ts
|
132
|
+
const [signal, abort, filterAbortError] = makeAbortable({ timeout: 10000 });
|
133
|
+
const fetcher = (url) => fetch(url, {signal:signal()}).catch(filterAbortError); // filters abort errors
|
134
|
+
```
|
135
|
+
Returns an accessor for the signal and the abort callback.
|
136
|
+
|
137
|
+
Options are optional and include:
|
138
|
+
- `timeout`: time in Milliseconds after which the fetcher aborts automatically
|
139
|
+
- `noAutoAbort`: can be set to true to make a new source not automatically abort a previous request
|
140
|
+
*/
|
141
|
+
export function makeAbortable(options) {
|
142
|
+
let controller, timeout
|
143
|
+
const abort = (reason) => {
|
144
|
+
timeout && clearTimeout(timeout);
|
145
|
+
controller?.abort(reason);
|
146
|
+
}
|
147
|
+
const signal = () => {
|
148
|
+
if (!options.noAutoAbort && controller?.signal.aborted === false) abort("retry");
|
149
|
+
controller = new AbortController();
|
150
|
+
if (options.timeout) timeout = setTimeout(() => abort("timeout"), options.timeout)
|
151
|
+
return controller.signal;
|
152
|
+
}
|
153
|
+
const error = err => {
|
154
|
+
if (err.name === "AbortError") return undefined
|
155
|
+
throw err;
|
156
|
+
}
|
157
|
+
return [signal, abort, error]
|
158
|
+
}
|
159
|
+
|
160
|
+
/**
|
161
|
+
Creates and handles an AbortSignal with automated cleanup
|
162
|
+
```ts
|
163
|
+
const [signal, abort, filterAbortError] =
|
164
|
+
createAbortable();
|
165
|
+
const fetcher = (url) => fetch(url, { signal: signal() })
|
166
|
+
.catch(filterAbortError); // filters abort errors
|
167
|
+
```
|
168
|
+
Returns an accessor for the signal and the abort callback.
|
169
|
+
|
170
|
+
Options are optional and include:
|
171
|
+
- `timeout`: time in Milliseconds after which the fetcher aborts automatically
|
172
|
+
- `noAutoAbort`: can be set to true to make a new source not automatically abort a previous request
|
173
|
+
*/
|
174
|
+
|
175
|
+
export function abortable(options) {
|
176
|
+
const [signal, abort, filterAbortError] = makeAbortable(options);
|
177
|
+
onCleanup(abort);
|
178
|
+
return [signal, abort, filterAbortError];
|
179
|
+
}
|
180
|
+
|
181
|
+
function castError(err) {
|
182
|
+
if (err instanceof Error) return err;
|
183
|
+
return new Error(typeof err === "string" ? err : "Unknown error", { cause: err });
|
184
|
+
}
|
package/src/router.js
ADDED
@@ -0,0 +1,65 @@
|
|
1
|
+
// @ts-nocheck
|
2
|
+
import {signal,effect,batch,untrack,memo,wrap,context,useContext,onMount} from './reactive.ts'
|
3
|
+
// import {h} from './hyperscript.ts'
|
4
|
+
|
5
|
+
const Ctx = context()
|
6
|
+
const useRouter = () => useContext(Ctx)
|
7
|
+
export const useNavigate = () => wrap(useRouter(),'navigate')
|
8
|
+
export const useParams = () => wrap(useRouter(),'params')
|
9
|
+
export const useSearch = () => wrap(useRouter(),'search')
|
10
|
+
|
11
|
+
export function Router(props) {
|
12
|
+
const routes = props.children
|
13
|
+
const NotFound = ()=>'Not found'
|
14
|
+
const render = () => {console.log('render')}
|
15
|
+
// const loaded = signal(false)
|
16
|
+
const navigate = signal(null)
|
17
|
+
const params = signal(null)
|
18
|
+
const search = signal(null)
|
19
|
+
onMount(()=>{
|
20
|
+
window.addEventListener("popstate", (event) => {
|
21
|
+
batch(()=>{
|
22
|
+
const hash = parseHash(document.location.hash)
|
23
|
+
navigate(hash)
|
24
|
+
})
|
25
|
+
})
|
26
|
+
const hash = parseHash(document.location.hash)
|
27
|
+
navigate(hash)
|
28
|
+
})
|
29
|
+
const children = memo(() => {
|
30
|
+
let location = navigate()
|
31
|
+
const route = routes.find(r=>r.path===location.pathname)
|
32
|
+
return route?.component() || NotFound
|
33
|
+
})
|
34
|
+
return Ctx({navigate,params,search,children:()=>children})
|
35
|
+
}
|
36
|
+
|
37
|
+
function parseHash(s) {
|
38
|
+
const res = {pathname:'/'}
|
39
|
+
if (s[0]!=='#') return res
|
40
|
+
s = s.substr(1)
|
41
|
+
|
42
|
+
let i = s.indexOf('?')
|
43
|
+
if (i===-1) i = s.length
|
44
|
+
res.pathname += s.substr(0,i)
|
45
|
+
if (res.pathname==='') res.pathname = '/'
|
46
|
+
return res
|
47
|
+
}
|
48
|
+
|
49
|
+
function parsePathname(s) {
|
50
|
+
let i = s.indexOf('?', 0)
|
51
|
+
if (i===-1) i = s.length
|
52
|
+
return s.substr(0,i)
|
53
|
+
}
|
54
|
+
|
55
|
+
function parseSearch(s) {
|
56
|
+
|
57
|
+
}
|
58
|
+
|
59
|
+
export function Route(props) {
|
60
|
+
|
61
|
+
}
|
62
|
+
|
63
|
+
export function A(props) {
|
64
|
+
|
65
|
+
}
|
package/src/runtime.ts
CHANGED
@@ -1,4 +1,5 @@
|
|
1
|
-
|
1
|
+
// @ts-nocheck:
|
2
|
+
import {effect,untrack,root} from './reactive.ts'
|
2
3
|
import {SVGNamespace,SVGElements,ChildProperties,getPropAlias,Properties,Aliases,DelegatedEvents} from './constants.ts'
|
3
4
|
import type {Window,Mountable,Elem,Node} from './constants.ts'
|
4
5
|
|
@@ -22,7 +23,7 @@ export type Runtime = {
|
|
22
23
|
}
|
23
24
|
|
24
25
|
/**
|
25
|
-
|
26
|
+
Create `Runtime` for `window`
|
26
27
|
@param window
|
27
28
|
@group Internal
|
28
29
|
*/
|
@@ -54,7 +55,7 @@ export function runtime(window:Window):Runtime {
|
|
54
55
|
function spread(node:Elem, props:any = {}, skipChildren:boolean) {
|
55
56
|
const prevProps:any = {}
|
56
57
|
if (!skipChildren) effect(() => (prevProps.children = insertExpression(node, props.children, prevProps.children)))
|
57
|
-
effect(() => (props.ref?.call ?
|
58
|
+
effect(() => (props.ref?.call ? untrack(() => props.ref(node)) : (props.ref = node)))
|
58
59
|
effect(() => assign(node, props, true, prevProps, true))
|
59
60
|
return prevProps
|
60
61
|
}
|
@@ -344,10 +345,10 @@ function style(node:Node, value:any, prev:any) {
|
|
344
345
|
}
|
345
346
|
|
346
347
|
function toPropertyName(name:string):string {
|
347
|
-
return name.toLowerCase().replace(/-([a-z])/g, (_:
|
348
|
+
return name.toLowerCase().replace(/-([a-z])/g, (_:unknown, w:string) => w.toUpperCase());
|
348
349
|
}
|
349
350
|
|
350
|
-
function toggleClassKey(node:
|
351
|
+
function toggleClassKey(node:Node, key:string, value:boolean) {
|
351
352
|
const classNames = key.trim().split(/\s+/)
|
352
353
|
for (let i = 0, nameLen = classNames.length; i < nameLen; i++)
|
353
354
|
node.classList.toggle(classNames[i], value)
|
@@ -360,13 +361,13 @@ function appendNodes(parent:Node, array:Node[], marker:null|Node = null) {
|
|
360
361
|
|
361
362
|
// Slightly modified version of: https://github.com/WebReflection/udomdiff/blob/master/index.js
|
362
363
|
function reconcileArrays(parentNode:Node, a:Node[], b:Node[]) {
|
363
|
-
|
364
|
-
|
364
|
+
const bLength = b.length
|
365
|
+
let aEnd = a.length,
|
365
366
|
bEnd = bLength,
|
366
367
|
aStart = 0,
|
367
368
|
bStart = 0,
|
368
|
-
after = a[aEnd - 1].nextSibling,
|
369
369
|
map:Map<Node,number>|null = null;
|
370
|
+
const after = a[aEnd - 1].nextSibling
|
370
371
|
|
371
372
|
while (aStart < aEnd || bStart < bEnd) {
|
372
373
|
// common prefix
|
package/test/hyperscript.js
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
import {runtime} from '../src/runtime.ts'
|
2
2
|
import {hyperscript} from '../src/hyperscript.ts'
|
3
3
|
import {signal,root} from '../src/reactive.ts'
|
4
|
-
import {JSDOM} from '
|
4
|
+
import {JSDOM} from 'jsdom'
|
5
5
|
import {assertEquals} from '@std/assert'
|
6
6
|
|
7
7
|
const {window} = new JSDOM('<!DOCTYPE html>', {runScripts:'dangerously'})
|
@@ -36,7 +36,7 @@ testing('h with basic element', {skip:true}, async test => {
|
|
36
36
|
await test("number content", () => assertHTML(h('i',1), '<i>1</i>'))
|
37
37
|
await test("bigint content", () => assertHTML(h('i',2n), '<i>2</i>'))
|
38
38
|
await test("symbol content", () => assertHTML(h('i',Symbol('A')), '<i>Symbol(A)</i>'))
|
39
|
-
await test('regex content', () => assertHTML(h('b',/\w/), '<b>/\\w/</b>'))
|
39
|
+
// await test('regex content', () => assertHTML(h('b',/\w/), '<b>/\\w/</b>'))
|
40
40
|
await test("signal content", () => assertHTML(h('i',()=>1), '<i>1</i>'))
|
41
41
|
await test('array content', () => assertHTML(h('i',['A',1,2n]), '<i>A12</i>'))
|
42
42
|
await test('style attribute', () => assertHTML(h('hr',{style:'color:red'}), '<hr style="color: red;">'))
|
package/test/reactive.js
CHANGED
@@ -1,8 +1,7 @@
|
|
1
1
|
import {assertEquals,assert} from '@std/assert'
|
2
2
|
import {describe,it} from '@std/testing/bdd'
|
3
3
|
|
4
|
-
import {signal,effect,
|
5
|
-
import {wrap} from '../src/controlflow.js'
|
4
|
+
import {signal,effect,untrack,batch,memo,context,useContext,root,wrap} from '../src/reactive.ts'
|
6
5
|
|
7
6
|
describe('signal', () => {
|
8
7
|
const a = signal(1)
|
@@ -14,6 +13,15 @@ describe('signal', () => {
|
|
14
13
|
assertEquals(a(null),null)
|
15
14
|
})
|
16
15
|
|
16
|
+
describe('signal with equals option', () => {
|
17
|
+
const n = signal(0,{equals:false})
|
18
|
+
let m = 0
|
19
|
+
effect(() => m += 1 + n() )
|
20
|
+
assertEquals(m,1)
|
21
|
+
assertEquals(n(0),0)
|
22
|
+
assertEquals(m,2)
|
23
|
+
})
|
24
|
+
|
17
25
|
describe('effect', () => {
|
18
26
|
const n = signal(1)
|
19
27
|
let m = 0
|
@@ -24,10 +32,10 @@ describe('effect', () => {
|
|
24
32
|
assertEquals(m,3)
|
25
33
|
})
|
26
34
|
|
27
|
-
describe('
|
35
|
+
describe('untrack',()=>{
|
28
36
|
const n = signal(1)
|
29
37
|
let m = 0
|
30
|
-
effect(()=>m =
|
38
|
+
effect(()=>m = untrack(n))
|
31
39
|
assertEquals(m,1)
|
32
40
|
n(2)
|
33
41
|
assertEquals(m,1)
|