diff --git a/packages/preview/typsy/0.2.1/LICENSE b/packages/preview/typsy/0.2.1/LICENSE new file mode 100644 index 0000000000..261eeb9e9f --- /dev/null +++ b/packages/preview/typsy/0.2.1/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/packages/preview/typsy/0.2.1/README.md b/packages/preview/typsy/0.2.1/README.md new file mode 100644 index 0000000000..9224ff13ec --- /dev/null +++ b/packages/preview/typsy/0.2.1/README.md @@ -0,0 +1,221 @@ +

typsy

+

Classes/structs, pattern matching, safe counters... and more!
Your one-stop library for programming tools not already in core Typst.

+ +--- + +_Pronounced 'tipsy', because I think that's funny and it's a nice pun on 'Typst'._ 😄 + +Provides tools for programming geeks: + +- classes (i.e. structs, custom types) +- pattern matching +- enums +- safe `counter`s and `state`s (no need to choose a unique string) +- trees-of-counters (i.e. subcounters) +- string formatting +- namespaces of objects that can be mutually referential +- runtime type checking + +## Installation + +Typst will autodownload packages on import: +```typst +#import "@preview/typsy:0.2.1" +``` + +## What's in the box? + +### Classes + +Classes with fields and methods: +```typst +#import "@preview/typsy:0.2.1": class, Int + +#let Adder = class( + fields: (x: Int), + methods: ( + add: (self, y) => {self.x + y} + ) +) +#let add_three = (Adder.new)(x: 3) +#let five = (add_three.add)(2) +``` + +### Pattern-matching + +Simple type checking: +```typst +#import "@preview/typsy:0.2.1": Array, Int, matches + +// Fixed-length case. +#matches(Array(Int, Int), (3, 4)) // true +// Variable-length case. +#matches(Array(..Int), (3, 4, 5, "not an int")) // false +``` + +More complicated match-case statements: +```typst +#import "@preview/typsy:0.2.1": Arguments, Int, Str, case, match, matches + +// Option 1: if/else +#let fn-with-multiple-signatures(..args) = { + if matches(Arguments(Int), args) { + // ... + } else if matches(Arguments(Str), args) { + // ... + } else if matches(Arguments(Str, level: Int), args) { + // ... + } else { + panic + } +} + +// Option 2: match/case +#let fn-with-multiple-signatures(..args) = { + match(args, + case(Arguments(Int), ()=>{ + // ... + }), + case(Arguments(Str), ()=>{ + // ... + }), + case(Arguments(Str, level: Int), ()=>{ + // ... + }), + ) +} +``` + +Observe the capitalisation. All patterns are capitalised to distinguish them from their corresponding type. + +### Enums + +Also using the same pattern-matching capabilities as above: +```typst +#import "@preview/typsy:0.2.1": case, class, enumeration, match, Int + +#let Shape = enumeration( + Rectangle: class(fields: (height: Int, width: Int)), + Circle: class(fields: (radius: Int)), +) +#let area(x) = { + match(x, + case(Shape.Rectangle, ()=>{ + x.height * x.width + }), + case(Shape.Circle, ()=>{ + calc.pi * calc.pow(x.radius, 2) + }), + ) +} +``` + +### Safe counters and states + +Counters without needing to cross your fingers and hope that you're using a unique string each time: +```typst +#import "@preview/typsy:0.2.1": safe-counter + +#let my-counter1 = safe-counter(()=>{}) +#let my-counter2 = safe-counter(()=>{}) +// ...these are different counters! +// (All anonymous functions have different identities to the compiler.) +``` + +Likewise safe states: +```typst +#import "@preview/typsy:0.2.1": safe-state +#let my-state1 = safe-state(()=>{}, "hello") +#let my-state2 = safe-state(()=>{}, "world") +// These are different states. The second argument is the default value. +``` + +### Tree counters / subcounters + +Create trees of counters, including using existing counters as starting points. This is particularly useful for creating theorem counters that increment with the heading. +```typst +#import "@preview/typsy:0.2.1": tree-counter + +// Set up counters +#let heading-counter = tree-counter(heading, level: 1) +#let theorem-counter = (heading-counter.subcounter)(()=>{}) // uses safe-counter internally! +#let corollary-counter = (theorem-counter.subcounter)(()=>{}) + +// Usage +#set heading(numbering: "1") +#let theorem(doc) = [Theorem #(theorem-counter.take)(): #doc] +#let corollary(doc) = [Corollary #(corollary-counter.take)(): #doc] += First Section +#theorem[Let ...] // Theorem 1.1: Let ... +#theorem[Let ...] // Theorem 1.2: Let ... +#corollary[Let ...] // Corollary 1.2.1: Let ... += Second Section +#theorem[Let ...] // Theorem 2.1: Let ... +``` + +### String formatting + +Rust-like string formatting: +```typst +#import "@preview/typsy:0.2.1": fmt, panic-fmt + +#let msg = fmt("Invalid input `{}`, expected `{}`.", foo, bar) + +// shorthand for `panic(fmt(...))` +#panic-fmt("Invalid input `{}`, expected `{}`.", foo, bar) +``` + +### Runtime type checking + +Wrap functions to check their inputs and outputs. This builds on top of the pattern-matching capablities above. +```typst +#import "@preview/typsy:0.2.1": Arguments, typecheck + +#let add_integers = typecheck(Arguments(Int, Int), Int, (x, y) => x + y) +#let five = add_integers(2, 3) // ok +#add_integers("hello ", "world") // panic! +``` + +### Namespaces of mutually-referential objects + +Build a namespace by providing lambda functions which return their object. Access any object in a namespace via `ns(object-name)`: +```typst +#import "@preview/typsy:0.2.1": namespace + +#let ns = namespace( + foo: ns => { + let foo(x) = if x == 0 {"FOO"} else {ns("bar")(x - 1)} + foo + }, + bar: ns => { + let bar(x) = if x == 0 {"BAR"} else {ns("foo")(x - 1)} + bar + }, +) +#let foo = ns("foo") +#assert.eq(foo(3), "BAR") +#assert.eq(foo(4), "FOO") +``` +For example, this can be used to implement mutually-recursive functions. + +## Documentation + +All objects have detailed docstrings indicating their usage; see those for details. + +The examples above demonstrate nearly every object in the public API. In addition to those above, the list of patterns that can be used for pattern-matching are: +```typst +Any, Arguments, Array, Bool, Bytes, Class, Content, Counter, Datetime, +Decimal, Dictionary, Duration, Float, Function, Int, Label, Literal, +Location, Module, Named, Never, None, Pattern, Pos, Ratio, Refine, +Regex, Selector, State, Str, Symbol, Type, Union, Version +``` +(They are capitalised to distinguish them from the underlying type.) + +## FAQ + +**Similar libraries:** + +- [elembic](https://github.com/pgbiel/elembic) offers a very different way to create custom classes. +- [valkyrie](https://github.com/typst-community/valkyrie) offers object parsing that is somewhat similar to our type matching. +- [headcount](https://github.com/jbirnick/typst-headcount) and [rich-counters](https://typst.app/universe/package/rich-counters/) also offer tree counters. (Though I find our approach a bit simpler, and safer due to our `()=>{}`-using safe counters.) +- [oxifmt](https://github.com/PgBiel/typst-oxifmt) offers Rust-like string formatting. Theirs is far more developed and better than what we have; I just like avoiding dependencies. diff --git a/packages/preview/typsy/0.2.1/src/classes.typ b/packages/preview/typsy/0.2.1/src/classes.typ new file mode 100644 index 0000000000..f2dfa7add0 --- /dev/null +++ b/packages/preview/typsy/0.2.1/src/classes.typ @@ -0,0 +1,308 @@ +#import "./format.typ": panic-fmt +#import "./match.typ": Any, Class, Dictionary, Function, Int, Literal, Pattern, Str, matches, pattern-repr + +#let _checktype(name, value, pattern) = { + if not matches(pattern, value) { + panic-fmt( + "For `{}`, expected `{}`, received `{}` of type `{}`.", + name, + pattern-repr(pattern), + repr(value), + repr(type(value)), + ) + } +} + +/// - fn (function): str->any +/// - names (array): +/// -> dictionary str=>any +#let _fn2dict(fn, names) = { + let out = (:) + for name in names { + out.insert(name, fn(name)) + } + out +} + +#let _make_cls(new, name, fields, methods, tag) = { + let out = (new: new, name: name, fields: fields, methods: methods, tag: tag, __typsy_sentinel_is_class: true) + // Make it possible to use classes in pattern-matching. + // We inspect specifically the `new` field as that should be enough to get uniqueness; in particular it closes over + // `tag`. + out + Dictionary(meta: Dictionary(cls: Dictionary(new: Literal(new), ..Any), ..Any), ..Any) +} + +#let _class_or_namespace(name: none, fields: none, methods: none, tag: none, call_on_dict: none) = { + if name != none { + _checktype("name", name, Str) + } + _checktype("fields", fields, Dictionary(..Any)) + _checktype("methods", methods, Dictionary(..Any)) + let methods_keys = methods.keys() + let reserved_fields = ("meta",) + for (argname, pattern) in fields.pairs() { + _checktype(argname, argname, Str) + if type(pattern) == type { + panic-fmt( + "For `{}`, received a type annotation `{}`, but expected a pattern. For example, you should write `class(fields: (x: Int))` rather than `class(fields: (x: int))`. This was a breaking change between typsy:0.1.0 and typsy:0.2.0", + argname, + repr(pattern), + ) + } + _checktype(argname, pattern, Pattern) + if methods_keys.contains(argname) { + panic-fmt("`{}` is present in both `fields` and `methods`", argname) + } + if reserved_fields.contains(argname) { + panic-fmt("`{}` is reserved and cannot be used in `fields`.", argname) + } + } + for (methodname, method) in methods.pairs() { + _checktype(methodname, methodname, Str) + _checktype(methodname, method, Function) + if reserved_fields.contains(methodname) { + panic-fmt("`{}` is reserved and cannot be used in `methods`.", methodname) + } + } + let new(..init_args) = { + if init_args.pos().len() != 0 { + panic-fmt("Do not call type constructors with positional arguments. Got `{}`", repr(init_args.pos())) + } + + let self_dict = (:) + for (initname, value) in init_args.named().pairs() { + let expected = fields.at(initname, default: auto) + if expected == auto { + panic-fmt("Got unexpected init argument `{}`.", initname) + } + _checktype(initname, value, expected) + self_dict.insert(initname, value) + } + for (initname, method) in methods.pairs() { + self_dict.insert(initname, method) + } + let repr_pieces = () + if name != none { + repr_pieces.push(name) + } + repr_pieces.push(repr(self_dict)) + let meta = ( + // Provide `cls` to allow easy self-recursion. + // Mutual recursion should be handled by using a `namespace`. + cls: _make_cls(new, name, fields, methods, tag), + repr: repr_pieces.join(""), + ) + self_dict.insert("meta", meta) + call_on_dict(self_dict) + } + _make_cls(new, name, fields, methods, tag) +} + +#let _call_on_dict(self_dict) = { + // This is a sneaky trick. The only kind of recursive data structure in Typst seems to be self-recursive + // functions. In particular this means that if were to have done e.g. + // `self_dict.insert(name, (..args)=>method(self, ..args))` + // above then this would not have worked! We would have captured the *old* version of `self`, which still + // only has some methods filled in. + let self_call(attrname) = { + let value = self_dict.at(attrname) + if type(value) == function { + (..args) => value(_fn2dict(self_call, self_dict.keys()), ..args) + } else { + value + } + } + _fn2dict(self_call, self_dict.keys()) +} + +/// Defines a class with attributes and methods. (Similar to Rust or Python.) +/// +/// *Example* +/// +/// ```typst +/// #{ +/// let Adder = class( +/// fields: (x: Int), +/// methods: ( +/// add: (self, y) => {self.x + y} +/// ) +/// ) +/// let add_three = (Adder.new)(x: 3) +/// let five = (add_three.add)(2) +/// } +/// +/// ``` +/// +/// *Notes:* +/// +/// - Method lookup (but not field lookup) requires brackets around the access. +/// - To access the class object (`Adder` itself in the above example) from within a method, then use `self.meta.cls`. +/// For example, this means that a new instance of the class can be instantiated via `(self.meta.cls.new)(...)`. +/// _Simply using the name of the class object will not work, as it does not yet exist whilst the methods are being +/// defined. (In this way `self.meta.cls` handles the case of simple recursion. And if you need mutual recursion +/// between two different classes/functions/etc, then see `namespace`.)_ +/// +/// *Returns:* +/// +/// The class object, which may later be instantiated via its `.new` method. +/// +/// *Arguments:* +/// +/// - name (none, str): an optional name for the class. Used in error messages. +/// - fields (dictionary): a mapping str=>type defining the types of the arguments that must be passed at initialisation. +/// - methods (dictionary): a mapping str=>function defining the methods available. +/// - tag (function): an optional place to add `class(..., tag: ()=>{})`. If not provided then all class objects with +/// the same fields and methods will compare equal. If provided then (as all anonymous functions are distinct), this +/// will make the class unique. +#let class(name: none, fields: (:), methods: (:), tag: none) = { + _class_or_namespace(name: name, fields: fields, methods: methods, tag: tag, call_on_dict: _call_on_dict) +} + +#let test-doc() = { + let Adder = class( + fields: (x: Int), + methods: ( + add: (self, y) => { self.x + y }, + ), + ) + let add_three = (Adder.new)(x: 3) + let five = (add_three.add)(2) + assert.eq(five, 5) +} + +#let test-basic() = { + let ArgTest = class(fields: (x: Int)) + let foo = (ArgTest.new)(x: 3) + assert.eq(foo.x, 3) +} + +#let panic-on-basic() = { + let ArgTest = class(fields: (x: Int)) + let foo = (ArgTest.new)(x: "not an int") +} + +#let test-self-recursive() = { + let self_recursive_construct_test = class( + fields: (x: Int), + methods: ( + add_one: self => { + (self.meta.cls.new)(x: self.x + 1) + }, + ), + ) + assert.eq(((self_recursive_construct_test.new)(x: 3).add_one)().x, 4) +} + +#let test-mutually-recursive-methods() = { + let mutally_recursive_methods_test = class( + fields: (x: Int), + methods: ( + baz: (self, x) => { + (self.bar)(x) + }, + bar: (self, x) => { + if x == 0 { + 5 + } else { + (self.baz)(x - 1) + } + }, + ), + ) + assert.eq(((mutally_recursive_methods_test.new)(x: 3).baz)(4), 5) +} + +#let test-tag() = { + let Foo1 = class() + let Foo2 = class() + let Foo3 = class(tag: () => {}) + let Foo4 = class(tag: () => {}) + assert.eq(Foo1, Foo2) + assert.ne(Foo3, Foo4) + + let foo1 = (Foo1.new)() + let foo2 = (Foo2.new)() + let foo3 = (Foo3.new)() + let foo4 = (Foo4.new)() + assert.eq(foo1, foo2) + assert.ne(foo3, foo4) +} + +#let test-unsugared-ns() = { + let basic_ns_test = class( + methods: ( + Foo: ns => class( + name: "Foo", + fields: (x: Int), + methods: ( + to_bar: self => ((ns.Bar)().new)(y: self.x), + ), + ), + Bar: ns => class( + name: "Bar", + fields: (y: Int), + methods: ( + to_foo: self => ((ns.Foo)().new)(x: self.y), + ), + ), + ), + ) + let foo = (((basic_ns_test.new)().Foo)().new)(x: 3) + assert.eq(foo.meta.repr, "Foo(x: 3, to_bar: (..) => ..)") + assert.eq((foo.to_bar)().meta.repr, "Bar(y: 3, to_foo: (..) => ..)") + + let bar = (((basic_ns_test.new)().Bar)().new)(y: 2) + assert.eq(bar.meta.repr, "Bar(y: 2, to_foo: (..) => ..)") + assert.eq((bar.to_foo)().meta.repr, "Foo(x: 2, to_bar: (..) => ..)") +} + +#let test-pattern-match-instances() = { + let Foo = class(fields: (x: Int)) + let Bar1 = class(fields: (y: Int), tag: () => {}) + let Bar2 = class(fields: (y: Int), tag: () => {}) + let Bar3 = class(fields: (y: Int)) + + let x = (Foo.new)(x: 3) + let y1 = (Bar1.new)(y: 4) + let y2 = (Bar2.new)(y: 4) + let y3 = (Bar3.new)(y: 4) + + assert(matches(Foo, x)) + assert(not matches(Bar1, x)) + assert(not matches(Bar2, x)) + assert(not matches(Bar3, x)) + + assert(not matches(Foo, y1)) + assert(matches(Bar1, y1)) + assert(not matches(Bar2, y1)) + assert(not matches(Bar3, y1)) + + assert(not matches(Foo, y2)) + assert(not matches(Bar1, y2)) + assert(matches(Bar2, y2)) + assert(not matches(Bar3, y2)) + + assert(not matches(Foo, y3)) + assert(not matches(Bar1, y3)) + assert(not matches(Bar2, y3)) + assert(matches(Bar3, y3)) + + // Identical classes + let bar4 = class(fields: (y: Int)) + assert(matches(bar4, y3)) +} + +#let test-pattern-match-class-object() = { + assert(matches(Class, class())) + assert(matches(Class, class(name: "hi"))) + assert(matches(Class, class(fields: (x: Int)))) + assert(not matches(Class, 4.0)) + assert(not matches(Class, (name: "hi"))) +} + +#let test-pattern-match-class-doc() = { + let Adder = class(fields: (x: Int), methods: (foo: (self, y) => self.x + y)) + assert(matches(Class, Adder)) + let adder = (Adder.new)(x: 3) + assert(matches(Adder, adder)) +} diff --git a/packages/preview/typsy/0.2.1/src/enumeration.typ b/packages/preview/typsy/0.2.1/src/enumeration.typ new file mode 100644 index 0000000000..89441e1722 --- /dev/null +++ b/packages/preview/typsy/0.2.1/src/enumeration.typ @@ -0,0 +1,88 @@ +#import "./match.typ": case, match, matches +#import "./format.typ": panic-fmt +#import "./classes.typ": Class, Int, class + +/// An enumeration over different classes. +/// +/// *Example:* +/// +/// ```typst +/// #let Shape = enumeration( +/// Rectangle: class(fields: (height: Int, width: Int)), +/// Circle: class(fields: (radius: Int)), +/// ) +/// +/// #let area(x) = { +/// match(x, +/// case(Shape.Rectangle, ()=>{ +/// x.height * x.width +/// }), +/// case(Shape.Circle, ()=>{ +/// calc.pi * calc.pow(x.radius, 2) +/// }), +/// ) +/// } +/// +/// #let circ = (Shape.Circle.new)(radius: 3) +/// #let area_circ = area(circ) +/// ``` +/// +/// **Returns:** +/// +/// A new enumeration (which is basically just a namespace of classes). +/// +/// **Arguments:** +/// +/// - items (arguments): any number of named classes. +#let enumeration(..items) = { + if items.pos().len() != 0 { + panic-fmt("`enumeration` should not be called with positional arguments. Got {}.", repr(items.pos().len())) + } + for (key, value) in items.named().pairs() { + if not matches(Class, value) { + panic-fmt("Every value in an `enumeration` must be a `class`.") + } + } + if items.named().values().dedup().len() != items.named().len() { + panic( + "Got duplicate entries in the enumeration. Consider adding tags: `class(..., tag: ()=>{})`. This " + + "exploits a trick in which all anonymous functions are distinct from each other, so it offers a way " + + "to distinguish structurally-identical classes.", + ) + } + items.named() +} + +#let test-pattern-match() = { + let Shape = enumeration( + Rectangle: class(fields: (height: Int, width: Int)), + Circle: class(fields: (radius: Int)), + ) + let area(x) = { + match( + x, + case(Shape.Rectangle, () => { + x.height * x.width + }), + case(Shape.Circle, () => { + // Good approximation to pi. :D + // (Better for testing correctness below.) + 3 * calc.pow(x.radius, 2) + }), + ) + } + assert.eq(area((Shape.Rectangle.new)(height: 3, width: 2)), 6) + assert.eq(area((Shape.Circle.new)(radius: 4)), 48) +} + +#let panic-on-not-class() = { + enumeration(foo: 3) +} + +#let panic-on-equal-classes() = { + enumeration(foo: class(), bar: class()) +} + +#let test-no-panic-on-tagged-classes() = { + enumeration(foo: class(tag: () => {}), bar: class(tag: () => {})) +} diff --git a/packages/preview/typsy/0.2.1/src/format.typ b/packages/preview/typsy/0.2.1/src/format.typ new file mode 100644 index 0000000000..71138114b0 --- /dev/null +++ b/packages/preview/typsy/0.2.1/src/format.typ @@ -0,0 +1,43 @@ +/// Lightweight Rust-like formatting. +/// +/// This will simply iterate over the string, replacing each instance of `{}` with one of the provided arguments. +/// See also the `oxifmt` library for the more heavyweight alternative. +/// +/// *Usage:* +/// +/// ```typst +/// #let foo = "Hello" +/// #let bar = "world" +/// #let hello_world = fmt("{} {}, hello!", foo, bar) +/// #assert(hello_world, "Hello world, hello!") +/// ``` +/// +/// *Returns:* +/// +/// The formatted string. +/// +/// *Arguments:* +/// +/// - format (str): e.g. "hello {}" +/// - args (arguments): e.g. "world" +#let fmt(format, ..args) = { + assert.eq( + args.named().len(), + 0, + message: "`fmt` does not consume any named arguments. Got `" + repr(args.named().keys()) + "`", + ) + let pieces = format.split("{}") + let out = (pieces.at(0),) + for (arg, piece) in args.pos().zip(pieces.slice(1), exact: true) { + out.push(arg) + out.push(piece) + } + out.join("") +} + +/// Short for `panic(fmt(fmt, ..args))` +/// - format (str): e.g. "hello {}" +/// - args (arguments): e.g. "world" +#let panic-fmt(format, ..args) = { + panic(fmt(format, ..args)) +} diff --git a/packages/preview/typsy/0.2.1/src/lib.typ b/packages/preview/typsy/0.2.1/src/lib.typ new file mode 100644 index 0000000000..3475a431b5 --- /dev/null +++ b/packages/preview/typsy/0.2.1/src/lib.typ @@ -0,0 +1,13 @@ +#import "./classes.typ": class +#import "./enumeration.typ": enumeration +#import "./format.typ": fmt, panic-fmt +#import "./match.typ": ( + Any, Arguments, Array, Bool, Bytes, Class, Content, Counter, Datetime, Decimal, Dictionary, Duration, Float, + Function, Int, Label, Literal, Location, Module, Named, Never, None, Pattern, Pos, Ratio, Refine, Regex, Selector, + State, Str, Symbol, Type, Union, Version, case, match, matches, pattern-repr, +) +#import "./namespace.typ": namespace +#import "./safe-counters.typ": safe-counter +#import "./safe-state.typ": safe-state +#import "./tree-counters.typ": tree-counter +#import "./typecheck.typ": typecheck diff --git a/packages/preview/typsy/0.2.1/src/match.typ b/packages/preview/typsy/0.2.1/src/match.typ new file mode 100644 index 0000000000..610af80f94 --- /dev/null +++ b/packages/preview/typsy/0.2.1/src/match.typ @@ -0,0 +1,810 @@ +#import "./format.typ" +#import "./format.typ": fmt, panic-fmt + +/// Checks if `obj` matches the `pattern`. +/// +/// *Examples:* +/// +/// ```typst +/// matches(Array(Int), (3, 4, 5, "not an int")) // false +/// ``` +/// +/// ```typst +/// let fn-with-multiple-signatures(..args) = { +/// if matches(Arguments(Int), args) { +/// // ... +/// } else if matches(Arguments(Str), args) { +/// // ... +/// } else if matches(Arguments(Str, level: Int), args) { +/// // ... +/// } else { +/// // ... +/// } +/// } +/// ``` +/// +/// *Returns:* +/// +/// Either `true` or `false`. +/// +/// *Arguments:* +/// +/// - pattern (Pattern): the pattern to check against, e.g. `Array(Dictionary(name: Str, email: Str))`. Note that the +/// underlying Typst types are not valid here. This because, for example, Typst does not have a way to indicate the +/// types of the elements of an `array`. In addition, `typsy.class`es can be used directly as patterns. +/// - obj (any): the object to check, e.g. `((name: "Tim", email: "tim@example.com"),)`. +#let matches(pattern, obj) = { + if type(pattern) == function { + panic-fmt( + "Cannot pattern-match on a function. This usually occurs when writing e.g. `matches(Array, (1, none))` " + + "rather than `matches(Array(Any), (1, none))`. Got pattern `{}`.", + repr(pattern), + ) + } + if type(pattern) == type { + panic-fmt( + "Cannot pattern-match on a type. This usually occurs when writing e.g. `matches(int, 3)` instead of " + + "`matches(Int, 3)`. Got pattern `{}`.", + repr(pattern), + ) + } + if type(pattern) != dictionary { + panic-fmt( + "Cannot pattern-match on a pattern of type `{}`. Got pattern `{}`.", + repr(type(pattern)), + repr(pattern), + ) + } + (pattern.__typsy_sentinel_match.match)(obj) +} + +/// Used with `match`. +/// +/// *Arguments:* +/// +/// - pattern (Pattern): any kind of pattern. +/// - fn (function): a function `()=>any`. +#let case(pattern, fn) = (pattern: pattern, fn: fn) +/// Pattern-matching. See also `enumeration` for where this feature really shines. +/// +/// This is syntactic sugar around the lower-level `matches` function. +/// +/// *Example:* +/// +/// ```typst +/// #let foo = (3, 4) +/// #let bar = match(foo, +/// case(Array(Int), ()=>{ +/// foo.at(0) +/// }), +/// case(Array(Int, Int), ()=>{ +/// foo.at(0) + foo.at(1) +/// }), +/// ) +/// ``` +/// +/// *Returns:* +/// +/// The output of the first `case` statement that the argument matches. +/// +/// *Arguments:* +/// +/// - obj (any): the object to pattern-match. +/// - cases (arguments): any number of `case`s. +#let match(obj, ..cases) = { + if cases.named().len() != 0 { + panic-fmt("`match` does not accept any named arguments, received `{}`.", repr(cases.named().keys())) + } + for case in cases.pos() { + if matches(case.pattern, obj) { return (case.fn)() } + } + panic-fmt("Did not match any case. Value was `{}`.", repr(obj)) +} + +/// Pretty-prints a pattern object. +/// +/// *Example:* +/// +/// `pattern-repr(Dictionary(x: Int, ..Any)) == "Dictionary(x: Int, ..Any)` +#let pattern-repr(pattern) = pattern.__typsy_sentinel_match.repr + +#let _match(match, repr) = (__typsy_sentinel_match: (match: match, repr: repr)) +#let _match_type(typ, repr) = _match(obj => type(obj) == typ, repr) + +// +// Basic types +// + +#let Any = _match(obj => true, "Any") +#let Bool = _match_type(bool, "Bool") +#let Bytes = _match_type(bytes, "Bytes") +#let Content = _match_type(content, "Content") +#let Counter = _match_type(counter, "Counter") +#let Datetime = _match_type(datetime, "Datetime") +#let Decimal = _match_type(decimal, "Decimal") +#let Duration = _match_type(duration, "Duration") +#let Float = _match_type(float, "Float") +#let Function = _match_type(function, "Function") // Just a simple type as there is no way to verify its signature. +#let Int = _match_type(int, "Int") +#let Label = _match_type(label, "Label") +#let Location = _match_type(location, "Location") +#let Module = _match_type(module, "Module") +#let Never = _match(obj => false, "Never") // Useful extra addition +#let None = _match(obj => obj == none, "None") +#let Ratio = _match_type(ratio, "Ratio") +#let Regex = _match_type(regex, "Regex") +#let Selector = _match_type(selector, "Selector") +#let Str = _match_type(str, "Str") +#let Symbol = _match_type(symbol, "Symbol") +#let Type = _match_type(type, "Type") +#let Version = _match_type(version, "Version") + +// +// Generic types +// + +#let _unpack(fn) = x => fn(..x) +#let _generic-repr(generic, args) = { + let out = () + let variadic = none + let pos = none + let named = none + for x in args.pos() { + out.push(pattern-repr(x)) + } + for (name, val) in args.named() { + if name == "__typsy_sentinel_match" { + variadic = val.repr + } else if name == "__typsy_sentinel_pos" { + pos = pattern-repr(val) + } else if name == "__typsy_sentinel_named" { + named = pattern-repr(val) + } else { + out.push(fmt("{}: {}", name, pattern-repr(val))) + } + } + if pos != none { + out.push(fmt("..Pos({})", pos)) + } + if named != none { + out.push(fmt("..Named({})", named)) + } + if variadic != none { + out.push(fmt("..{}", variadic)) + } + if out.len() == 0 { + generic + "()" + } else { + fmt("{}({})", generic, out.join(", ")) + } +} +#let _with-repr(pattern, repr) = { + _match(pattern.__typsy_sentinel_match.match, repr) +} + +/// *Usage:* +/// +/// - `Array(Int)` is a pattern that would accept `(3,)` but not `(3, 4)`. +/// - `Array(Int, Int)` is a pattern that would accept `(3, 4)` but not `(3,)`. +/// - `Array(Int, Str)` is a pattern that would accept `(3, "hi")`. +/// - `Array(..Int)` is a pattern that would accept `(3,)` and `(3, 4)` and `(3, 4, 5)`. +/// - `Array(Bool, Str, ..Int)` is a pattern that would accept `(true, "hello")` and `(true, "hello", 3, 4, 5)` +/// +/// So positional arguments correspond to the types of the first elements on an array, and `..` unpacking corresponds to +/// the types of the tail. +#let Array(..eltypes) = { + let pos = eltypes.pos() + let named = eltypes.named() + let array-repr = () => _generic-repr("Array", eltypes) + if named.len() == 0 { + _match( + obj => type(obj) == array and pos.len() == obj.len() and pos.zip(obj).all(_unpack(matches)), + array-repr(), + ) + } else { + if named.keys() == ("__typsy_sentinel_match",) { + if pos.len() == 0 and named == Any { + _match_type(array, array-repr()) // Fastpath + } else { + _match( + obj => ( + type(obj) == array + and pos.len() <= obj.len() + and pos.zip(obj.slice(0, pos.len()), exact: true).all(_unpack(matches)) + and obj.slice(pos.len()).all(matches.with(named)) + ), + array-repr(), + ) + } + } else { + panic-fmt( + "`Array` should either be called with positional arguments e.g. `Array(Int, Str)`, or with a variadic " + + "argument e.g. `Array(..Int)`, or both e.g. `Array(Int, Str, ..Int)`. However received keywords " + + "`{}` instead.", + repr(named.keys()), + ) + } + } +} +/// *Usage:* +/// +/// - `Dictionary(..Int)` is a pattern that would accept `(foo: 3)` and `(foo: 3, bar: 4)` but not `(foo: 3, bar: "x")`. +/// - `Dictionary(bar: Str, ..Int)` is a pattern that would accept `(foo: 3, bar: "x")` but not `(foo: 3)` +/// or `(foo: 3, bar: 4)`. +/// +/// So the named arguments correspond to type of the value in key-value pairs with that specific key, and `..` unpacking +/// corresponds to the the type of the value for all other keys. (And recall that Typst only allows for string-typed +/// keys, so we do not need to annotate those.) +#let Dictionary(..valtypes) = { + if valtypes.pos().len() != 0 { + panic-fmt( + "`Dictionary` should either be called with zero positional arguments. Got {} positional arguments instead.", + repr(valtypes.pos().len()), + ) + } + let dictionary-repr = () => _generic-repr("Dictionary", valtypes) + let named = valtypes.named() + let valtype = ( + __typsy_sentinel_match: named.remove("__typsy_sentinel_match", default: Never.__typsy_sentinel_match), + ) + if valtype == Any and named.len() == 0 { return _match_type(dictionary, dictionary-repr()) } // Fastpath + if valtype == Any and named.len() == 1 { + // Fastpath for a particularly common case. + let ((key, pat),) = named.pairs() + return _match( + obj => type(obj) == dictionary and obj.keys().contains(key) and matches(pat, obj.at(key)), + dictionary-repr(), + ) + } + _match( + obj => ( + type(obj) == dictionary + and obj.pairs().all(kv => matches(named.at(kv.at(0), default: valtype), kv.at(1))) + and named.keys().all(n => obj.keys().contains(n)) + ), + dictionary-repr(), + ) +} +/// For use with `Arguments`. +#let Pos(eltype) = (__typsy_sentinel_pos: eltype) +/// For use with `Arguments`. +#let Named(valtype) = (__typsy_sentinel_named: valtype) +/// *Usage:* +/// +/// - `Arguments(Str, Bool)` is a pattern that would match the `arguments` to `some-fn("foo", true)`. +/// - `Arguments(Str, level: Int)` is a pattern that would match the `arguments` to `some-fn("foo", level: 1)`. +/// - `Arguments(level: Int, sep: Str)` is a pattern that would match the `arguments` to `some-fn(level: 1, sep: ",")`. +/// - `Arguments(..Any)` is a pattern that would match all `arguments` to any function. +/// - `Arguments(..Int)` is a pattern that would match the `arguments` to `some-fn(3, 4, foo: 5, bar: 6)`. +/// - `Arguments(..Pos(Int))` is a pattern that would match the `arguments` to `some-fn(3, 4)`. +/// - `Arguments(..Named(Int))` is a pattern that would match the `arguments` to `some-fn(foo: 5, bar: 6)`. +/// +/// That is, we can specify both positional and named arguments straightforwardly. We can specify variadic positional +/// arguments using `..Pos(Foo)` and variadic named arguments using `..Named(Foo)`, and variadic positional+named +/// arguments via `..Foo`. +#let Arguments(..args) = { + let arguments-repr = () => _generic-repr("Arguments", args) + let named = args.named() + + let var = named.remove("__typsy_sentinel_match", default: auto) + let varpos = named.remove("__typsy_sentinel_pos", default: auto) + let varnamed = named.remove("__typsy_sentinel_named", default: auto) + let pos = if var == auto { + if varpos == auto { + Array(..args.pos()) + } else { + Array(..args.pos(), ..varpos) + } + } else { + if varpos == auto { + Array(..args.pos(), __typsy_sentinel_match: var) + } else { + panic-fmt( + "`Arguments` called with both variadic and variadic-positional arguments, e.g. " + + "`Arguments(..Foo, ..Pos(Bar))`", + ) + } + } + let named = if var == auto { + if varnamed == auto { + Dictionary(..named) + } else { + Dictionary(..named, ..varnamed) + } + } else { + if varnamed == auto { + Dictionary(..named, __typsy_sentinel_match: var) + } else { + panic-fmt( + "`Arguments` called with both variadic and variadic-named arguments, e.g. " + + "`Arguments(..Foo, ..Named(Bar))`", + ) + } + } + + _match(obj => type(obj) == arguments and matches(pos, obj.pos()) and matches(named, obj.named()), arguments-repr()) +} +/// *Usage:* +/// +/// - `State(Int)` would match `state("foo", 4)` +/// +/// That is, the pattern specifies the value of the state. +#let State(statetype) = _match( + obj => type(obj) == state and matches(statetype, obj.get()), + fmt("State({})", pattern-repr(statetype)), +) + +// +// Miscellaneous +// + +/// *Usage:* +/// +/// - `Literal(3)` is a pattern that would accept `3` but not `4`. +/// - `Literal(3, 4, "hi")` is a pattern that would accept `3` and `"hi"` but not `"bye"`. +/// +/// That is, this is a collection of values, at least one of which we must be equal to. +#let Literal(..values) = { + let named = values.named() + if named.len() != 0 { + panic-fmt("`Literal` should only be called with positional arguments. Got keywords `{}`.", repr(named.keys())) + } + let literal-repr = ("Literal(",) + for (i, val) in values.pos().enumerate() { + if i != 0 { + literal-repr.push(", ") + } + literal-repr.push(repr(val)) + } + literal-repr.push(")") + _match(obj => values.pos().contains(obj), literal-repr.join("")) +} +/// *Usage:* +/// +/// - `Union(Int, Str)` is a pattern that would accept `3` and `"hi"` but not `(foo: 4)`. +/// +/// That is, this is a collection of patterns, at least one of which we must match. +#let Union(..values) = { + let named = values.named() + if named.len() != 0 { + panic-fmt("`Union` should only be called with positional arguments. Got keywords `{}`.", repr(named.keys())) + } + _match(obj => values.pos().any(v => matches(v, obj)), _generic-repr("Union", values)) +} +/// Matches pattern objects themselves. Does *not* go via `Dictionary` because we're actually looking for the sentinel +// key that `Dictionary` uses to identify its default value. +#let Pattern = _match( + x => ( + type(x) == dictionary + and x.keys() == ("__typsy_sentinel_match",) + and type(x.__typsy_sentinel_match) == dictionary + and x.__typsy_sentinel_match.keys() == ("match", "repr") + and type(x.__typsy_sentinel_match.match) == function + and type(x.__typsy_sentinel_match.repr) == str + ), + "Pattern", +) + +/// Used for pattern-matching class objects themselves. Not to be confused with matching class instances. +/// +/// *Example:* +/// +/// ```typst +/// #let Adder = class(fields: (x: Int), methods: (foo: (self, y)=>self.x+y)) +/// #assert(matches(Class, Adder)) +/// #let adder = (Adder.new)(x: 3) +/// #assert(matches(Adder, adder)) +/// ``` +#let Class = _with-repr(Dictionary(__typsy_sentinel_is_class: Literal(true), ..Any), "Class") +/// Takes a pattern, and additionally requires that a function must return `true` in order for values to satisfy the +/// pattern. +/// +/// *Examples:* +/// +/// ```typst +/// #let Email = Refine(Str, x=>x.contains("@")) +/// #matches(Email, "hello@example.com") // true +/// #matches(Email, "hello world") // false +/// ``` +/// +/// This can also be used to create brand-new patterns by refining `Any`, e.g. we could reimplement `Literal` via +/// ```typst +/// #let MyLiteral(..values) = Refine(Any, x=>values.pos().contains(x)) +/// #matches(MyLiteral(3), 3) // true +/// #matches(MyLiteral(3), 4) // false +/// ``` +/// +/// **Returns:** +/// +/// A new pattern. +/// +/// **Arguments:** +/// +/// - pattern (Pattern): an existing pattern. +/// - predicate (function): a function ` -> bool`. It will only be called on items that already +/// match `pattern`. +#let Refine(pattern, predicate) = { + _match( + obj => matches(pattern, obj) and predicate(obj), + fmt("Refine({}, {})", pattern-repr(pattern), repr(predicate)), + ) +} + +#let panic-on-type() = { + matches(int, 3) +} + +#let panic-on-function() = { + matches(Array, (3,)) +} + +#let test-basic-types() = { + assert(matches(Any, "hi")) + assert(matches(Any, 1)) + assert(matches(Any, [some content])) + + assert(matches(Bool, true)) + assert(not matches(Bool, "not a bool")) + + assert(matches(Bytes, bytes("hello"))) + assert(not matches(Bytes, "hello")) + + assert(matches(Content, [hello there])) + assert(not matches(Content, "hello there")) + + assert(matches(Datetime, datetime(year: 1, month: 1, day: 1))) + assert(matches(Datetime, datetime(hour: 1, minute: 1, second: 1))) + assert(not matches(Datetime, "hello")) + + assert(matches(Decimal, decimal("3.0"))) + assert(not matches(Decimal, 3.0)) + + assert(matches(Duration, duration(hours: 1))) + assert(not matches(Duration, "duration")) + + assert(matches(Float, 3.0)) + assert(not matches(Float, 3)) + + assert(matches(Function, () => {})) + assert(not matches(Function, ())) + + assert(matches(Int, 3)) + assert(not matches(Int, 3.0)) + assert(not matches(Int, decimal("3"))) + + assert(matches(Label, )) + assert(not matches(Label, "hi")) + + assert(matches(Module, format)) + assert(not matches(Module, "format")) + + assert(not matches(Never, 3)) + assert(not matches(Never, "3")) + + assert(matches(None, none)) + assert(not matches(None, "none")) + + assert(matches(Ratio, 150%)) + assert(not matches(Ratio, 1 / 2)) + + assert(matches(Regex, regex("foo"))) + assert(not matches(Regex, "foo")) + + assert(matches(Selector, heading.where(level: 1))) + assert(not matches(Selector, heading)) + + assert(matches(Str, "hi")) + assert(not matches(Str, bytes("hi"))) + + assert(matches(Symbol, symbol("x"))) + assert(not matches(Symbol, "x")) + + assert(matches(Type, int)) + assert(matches(Type, str)) + assert(matches(Type, function)) + assert(not matches(Type, Int)) + assert(not matches(Type, Str)) + assert(not matches(Type, Function)) + + assert(matches(Version, version(0, 1, 2))) + assert(not matches(Version, (0, 1, 2))) + + assert(matches(Pattern, Any)) + assert(matches(Pattern, Int)) + assert(matches(Pattern, Function)) + assert(matches(Pattern, Dictionary(x: Int))) + assert(not matches(Pattern, int)) + assert(not matches(Pattern, (x: 3))) +} + +#let test-location() = { + context assert(matches(Location, here())) + assert(not matches(Location, "here")) +} +#test-location() + +#let test-arguments() = { + let id(..args) = args + assert(not matches(Arguments(..Any), 3)) + + assert(matches(Arguments(Int), id(3))) + assert(not matches(Arguments(Int), id(3, 4))) + assert(not matches(Arguments(Int), id(3, foo: 4))) + assert(not matches(Arguments(Int), id(foo: 4))) + assert(not matches(Arguments(Int), id(3.0))) + assert(matches(Arguments(Int, Str, Int), id(3, "hi", 4))) + assert(not matches(Arguments(Int, Str, Int), id(3, "hi", 4, 5))) + assert(not matches(Arguments(Int, Str, Int), id(3, 4, "hi", 5))) + + assert(matches(Arguments(Int, Str, foo: Int), id(3, "hi", foo: 4))) + assert(not matches(Arguments(Int, Str, foo: Int), id(3, "hi", foo: "hi"))) + assert(not matches(Arguments(Int, Str, foo: Int, bar: Array(Int)), id(3, "hi", foo: "hi"))) + assert(matches(Arguments(Int, Str, foo: Str, bar: Array(Int)), id(3, "hi", foo: "hi", bar: (3,)))) + assert(not matches(Arguments(Int, Str, foo: Int, bar: Array(Int)), id(3, "hi", foo: "hi", bar: (3.0,)))) + + assert(matches(Arguments(..Any), id(3))) + assert(matches(Arguments(..Any), id(3, 4))) + assert(matches(Arguments(..Any), id(3, 4, foo: "hi"))) + assert(matches(Arguments(..Any), id(3, 4, foo: "hi", bar: id(5)))) + + assert(matches(Arguments(..Int), id())) + assert(matches(Arguments(..Int), id(3))) + assert(matches(Arguments(..Int), id(3, 4))) + assert(not matches(Arguments(..Int), id(3, 4, foo: "hi"))) + assert(not matches(Arguments(..Int), id(3, "hi", foo: 4))) + assert(matches(Arguments(..Int), id(3, 4, foo: 4))) + + assert(matches(Arguments(..Pos(Int)), id())) + assert(matches(Arguments(..Pos(Int)), id(3))) + assert(matches(Arguments(..Pos(Int)), id(3, 4))) + assert(not matches(Arguments(..Pos(Int)), id(3, "4"))) + assert(not matches(Arguments(..Pos(Int)), id(3, 4, foo: 5))) + + assert(matches(Arguments(..Named(Int)), id())) + assert(matches(Arguments(..Named(Int)), id(foo: 3))) + assert(matches(Arguments(..Named(Int)), id(foo: 3, bar: 4))) + assert(not matches(Arguments(..Named(Int)), id(foo: 3, bar: "4"))) + assert(not matches(Arguments(..Named(Int)), id(3, 4, foo: 5))) + + assert(matches(Arguments(..Pos(Int), ..Named(Str)), id())) + assert(matches(Arguments(..Pos(Int), ..Named(Str)), id(3))) + assert(matches(Arguments(..Pos(Int), ..Named(Str)), id(3, foo: "hi"))) + assert(not matches(Arguments(..Pos(Int), ..Named(Str)), id(3, foo: 3))) + assert(not matches(Arguments(..Pos(Int), ..Named(Str)), id("hi", foo: 3))) + assert(matches(Arguments(Str, foo: Int, ..Pos(Int), ..Named(Str)), id("hi", foo: 3))) + assert(matches(Arguments(..Pos(Int), foo: Int, ..Named(Str)), id(3, foo: 3))) + + let def = Arguments(Str, Int, Float, foo: Int, ..Pos(Int), ..Named(Str)) + assert(matches(def, id("hi", 3, 3.0, 2, 5, foo: 3, bar: "bye"))) + assert(not matches(def, id(3, 3, 3.0, 2, 5, foo: 3, bar: "bye"))) + assert(not matches(def, id("hi", "3", 3.0, 2, 5, foo: 3, bar: "bye"))) + assert(not matches(def, id("hi", 3, "3.0", 2, 5, foo: 3, bar: "bye"))) + assert(not matches(def, id("hi", 3, 3.0, 2, 5, foo: "3", bar: "bye"))) + assert(not matches(def, id("hi", 3, 3.0, "2", 5, foo: 3, bar: 5))) + assert(not matches(def, id("hi", 3, 3.0, 2, 5, foo: 3, bar: 5))) +} + + +#let test-array() = { + assert(not matches(Array(Any), 3)) + assert(not matches(Array(Any), (hi: 3))) + + assert(matches(Array(), ())) + assert(not matches(Array(), (3,))) + assert(not matches(Array(), (3, 4))) + assert(matches(Array(Any), (3,))) + assert(matches(Array(Int), (3,))) + assert(not matches(Array(Int), ())) + assert(not matches(Array(Int), (3, 4))) + assert(matches(Array(Any, Any), (1, none))) + assert(matches(Array(Any, Any), (1, "hi"))) + assert(not matches(Array(Any, Any), ())) + assert(not matches(Array(Any, Any), (3,))) + assert(not matches(Array(Int, Any), (3,))) + assert(not matches(Array(Int, Int), (3,))) + + assert(matches(Array(Int, Str), (3, "hi"))) + assert(not matches(Array(Int, Str), ("hi", 3))) + + assert(matches(Array(Array(Int), Str), ((3,), "hi"))) + assert(not matches(Array(Array(Int), Str), ((3, 4), "hi"))) + + assert(matches(Array(..Int), ())) + assert(matches(Array(..Int), (3,))) + assert(matches(Array(..Int), (3, 4))) + assert(matches(Array(..Int), (3, 4, 5))) + assert(matches(Array(..Any), ())) + assert(matches(Array(..Any), (3, "4", none))) + assert(matches(Array(..Never), ())) + assert(not matches(Array(..Never), (3,))) + assert(matches(Array(..Array(Int)), ((3,), (4,), (5,)))) + assert(not matches(Array(..Array(Int)), ((3,), (4,), (5, 6)))) + + assert(not matches(Array(Int, Str, ..Int), ())) + assert(not matches(Array(Int, Str, ..Int), (3,))) + assert(matches(Array(Int, Str, ..Int), (3, "hi"))) + assert(matches(Array(Int, Str, ..Int), (3, "hi", 4))) + assert(matches(Array(Int, Str, ..Int), (3, "hi", 4, 5))) + assert(not matches(Array(Int, Str, ..Int), (3, "hi", 4, 5, "hi"))) + + assert(matches(Array(Int, Str, ..Any), (3, "hi", 4, 5))) + assert(matches(Array(Int, Str, ..Any), (3, "hi", 4, 5, "hi"))) +} + +#let panic-on-array-keywords() = { + Array(hi: 3) +} + +#let test-dictionary() = { + assert(not matches(Dictionary(), 3)) + assert(not matches(Dictionary(), (3,))) + + assert(matches(Dictionary(), (:))) + assert(matches(Dictionary(..Any), (:))) + assert(matches(Dictionary(..Int), (:))) + assert(matches(Dictionary(..Str), (:))) + + assert(matches(Dictionary(..Int), (hi: 3, bye: 3))) + assert(matches(Dictionary(..Any), (hi: 3, bye: 3))) + assert(matches(Dictionary(..Any), (hi: 3, bye: "foo"))) + assert(matches(Dictionary(..Union(Int, Str)), (hi: 3, bye: "foo"))) + assert(not matches(Dictionary(..Union(Int, Bytes)), (hi: 3, bye: "foo"))) + assert(not matches(Dictionary(..Union(Int, Str)), (hi: 3, bye: "foo", baz: 3.0))) + + assert(matches(Dictionary(hi: Int, bye: Int), (hi: 3, bye: 3))) + assert(not matches(Dictionary(hi: Int, bye: Int), (hi: 3, bye: "foo"))) + assert(matches(Dictionary(hi: Int, bye: Str), (hi: 3, bye: "foo"))) + assert(matches(Dictionary(hi: Int, bye: Str), (hi: 3, bye: "foo"))) + + assert(matches(Dictionary(hi: Int, bye: Str, ..Str), (hi: 3, bye: "foo", baz: "boom"))) + assert(not matches(Dictionary(hi: Int, bye: Str, ..Str), (hi: 3, bye: "foo", baz: 3))) + assert(matches(Dictionary(hi: Int, bye: Str, ..Any), (hi: 3, bye: "foo", baz: "boom"))) + assert(not matches(Dictionary(hi: Int, bye: Int, ..Any), (hi: 3, bye: "foo"))) + assert(not matches(Dictionary(hi: Int, bye: Int, ..Str), (hi: 3, bye: "foo"))) +} + +#let test-dictionary-incomplete() = { + assert(not matches(Dictionary(foo: Int, ..Any), (:))) +} + +#let test-dictionary-fastpath-single-element() = { + let pat = Dictionary(hi: Int, ..Any) + assert(matches(pat, (hi: 3))) + assert(matches(pat, (hi: 3, bye: 3))) + assert(matches(pat, (hi: 3, bye: "foo"))) + assert(not matches(pat, (:))) + assert(not matches(pat, (hi: "foo"))) + assert(not matches(pat, (bye: "foo"))) + assert(not matches(pat, (bye: 3))) +} + +#let panic-on-dictionary-args1() = { + Dictionary(Int) +} + +#let panic-on-dictionary-args2() = { + Dictionary(Int, Str) +} + +#let test-state() = { + let foo = state("kidger-match-test-state", 3) + context assert(matches(State(Any), foo)) + context assert(matches(State(Int), foo)) + context assert(not matches(State(Str), foo)) +} +#test-state() + +#let test-literal() = { + assert(matches(Literal(heading), heading)) + assert(matches(Literal(heading, page), heading)) + assert(not matches(Literal(page), heading)) + assert(matches(Literal(int), int)) + assert(not matches(Literal(int), str)) +} + +#let test-union() = { + assert(matches(Union(Int, Str), 3)) + assert(matches(Union(Int, Int), 3)) + assert(not matches(Union(Bytes, Str), 3)) + assert(matches(Union(Array(..Int), Array(..Str)), (3,))) + assert(not matches(Union(Array(..Int), Array(..Str)), (3.0,))) + assert(matches(Union(Array(..Union(Int, Float)), Array(..Str)), (3.0,))) +} + +#let test-refine() = { + let Email = Refine(Str, x => x.contains("@")) + assert(matches(Email, "hello@example.com")) + assert(not matches(Email, "hello world")) + let MyLiteral(..values) = Refine(Any, x => values.pos().contains(x)) + assert(matches(MyLiteral(3), 3)) + assert(not matches(MyLiteral(3), 4)) +} + +#let test-pattern-match() = { + assert.eq(match(3, case(Int, () => 4), case(Str, () => 5)), 4) +} + +#let panic-on-no-pattern-match() = { + match(3, case(Float, () => 4), case(Str, () => 5)) +} + +#let test-pattern-repr-basic() = { + assert.eq(pattern-repr(Any), "Any") + assert.eq(pattern-repr(Bool), "Bool") + assert.eq(pattern-repr(Bytes), "Bytes") + assert.eq(pattern-repr(Content), "Content") + assert.eq(pattern-repr(Counter), "Counter") + assert.eq(pattern-repr(Datetime), "Datetime") + assert.eq(pattern-repr(Decimal), "Decimal") + assert.eq(pattern-repr(Duration), "Duration") + assert.eq(pattern-repr(Float), "Float") + assert.eq(pattern-repr(Function), "Function") + assert.eq(pattern-repr(Int), "Int") + assert.eq(pattern-repr(Label), "Label") + assert.eq(pattern-repr(Location), "Location") + assert.eq(pattern-repr(Module), "Module") + assert.eq(pattern-repr(Never), "Never") + assert.eq(pattern-repr(None), "None") + assert.eq(pattern-repr(Ratio), "Ratio") + assert.eq(pattern-repr(Regex), "Regex") + assert.eq(pattern-repr(Selector), "Selector") + assert.eq(pattern-repr(Str), "Str") + assert.eq(pattern-repr(Symbol), "Symbol") + assert.eq(pattern-repr(Type), "Type") + assert.eq(pattern-repr(Version), "Version") +} + +#let test-pattern-repr-array() = { + assert.eq(pattern-repr(Array()), "Array()") + assert.eq(pattern-repr(Array(Int)), "Array(Int)") + assert.eq(pattern-repr(Array(Int, Str)), "Array(Int, Str)") + assert.eq(pattern-repr(Array(..Any)), "Array(..Any)") + assert.eq(pattern-repr(Array(Int, ..Any)), "Array(Int, ..Any)") + assert.eq(pattern-repr(Array(Int, Str, ..Any)), "Array(Int, Str, ..Any)") +} + +#let test-pattern-repr-dictionary() = { + assert.eq(pattern-repr(Dictionary()), "Dictionary()") + assert.eq(pattern-repr(Dictionary(x: Int)), "Dictionary(x: Int)") + assert.eq(pattern-repr(Dictionary(x: Int, y: Str)), "Dictionary(x: Int, y: Str)") + assert.eq(pattern-repr(Dictionary(..Ratio)), "Dictionary(..Ratio)") + assert.eq(pattern-repr(Dictionary(x: Int, ..Ratio)), "Dictionary(x: Int, ..Ratio)") + assert.eq(pattern-repr(Dictionary(x: Int, y: Str, ..Ratio)), "Dictionary(x: Int, y: Str, ..Ratio)") +} + +#let test-pattern-repr-arguments() = { + assert.eq(pattern-repr(Arguments()), "Arguments()") + assert.eq(pattern-repr(Arguments(Int)), "Arguments(Int)") + assert.eq(pattern-repr(Arguments(Int, Str)), "Arguments(Int, Str)") + assert.eq(pattern-repr(Arguments(x: Int)), "Arguments(x: Int)") + assert.eq(pattern-repr(Arguments(x: Int, y: Str)), "Arguments(x: Int, y: Str)") + assert.eq(pattern-repr(Arguments(Str, x: Int)), "Arguments(Str, x: Int)") + assert.eq(pattern-repr(Arguments(x: Int, ..Pos(Str))), "Arguments(x: Int, ..Pos(Str))") + assert.eq(pattern-repr(Arguments(x: Int, ..Named(Str))), "Arguments(x: Int, ..Named(Str))") + assert.eq(pattern-repr(Arguments(x: Int, ..Any)), "Arguments(x: Int, ..Any)") + assert.eq( + pattern-repr(Arguments(Bool, x: Int, ..Pos(Ratio), ..Named(Str))), + "Arguments(Bool, x: Int, ..Pos(Ratio), ..Named(Str))", + ) +} + +#let test-pattern-repr-unusual() = { + // state, literal, union, pattern, class, refine + assert.eq(pattern-repr(State(Int)), "State(Int)") + + assert.eq(pattern-repr(Literal()), "Literal()") + assert.eq(pattern-repr(Literal(3)), "Literal(3)") + assert.eq(pattern-repr(Literal(3, "hi")), "Literal(3, \"hi\")") + + assert.eq(pattern-repr(Union()), "Union()") + assert.eq(pattern-repr(Union(Int)), "Union(Int)") + assert.eq(pattern-repr(Union(Int, Str)), "Union(Int, Str)") + + assert.eq(pattern-repr(Pattern), "Pattern") + assert.eq(pattern-repr(Class), "Class") + + let refine-repr = pattern-repr(Refine(Int, x=>x>0)) + assert(refine-repr.starts-with("Refine(Int, ")) + assert(refine-repr.ends-with(")")) +} + diff --git a/packages/preview/typsy/0.2.1/src/namespace.typ b/packages/preview/typsy/0.2.1/src/namespace.typ new file mode 100644 index 0000000000..156e38a5e7 --- /dev/null +++ b/packages/preview/typsy/0.2.1/src/namespace.typ @@ -0,0 +1,97 @@ +#import "./classes.typ": _class_or_namespace, class, Int +#import "./format.typ": panic-fmt + +#let _call_on_dict(self_dict) = { + let self_call(attrname) = { + let value = self_dict.at(attrname) + assert.eq(type(value), function) + value(self_call) + } + self_call +} + +/// Builds a namespace of objects that may be mutually-referential. +/// +/// This ability to be mutually-referential is what distinguishes this object (and is the reason it exists) as compared +/// to just normal Typst code. +/// +/// *Example:* +/// +/// ```typst +/// #let ns = namespace( +/// foo: ns => { +/// let foo(x) = if x == 0 {"FOO"} else {ns("bar")(x - 1)} +/// foo +/// }, +/// bar: ns => { +/// let bar(x) = if x == 0 {"BAR"} else {ns("foo")(x - 1)} +/// bar +/// }, +/// ) +/// #let foo = ns("foo") +/// #assert.eq(foo(3), "BAR") +/// #assert.eq(foo(4), "FOO") +/// ``` +/// +/// *Returns:* +/// +/// The namespace object. +/// +/// *Arguments:* +/// +/// - objs (arguments): a collection of objects in the namespace. +#let namespace(..objs) = { + if objs.pos().len() != 0 { + panic-fmt("Cannot pass positional arguments to a namespace.") + } + ( + _class_or_namespace( + name: "namespace", + fields: (:), + methods: objs.named(), + tag: none, + call_on_dict: _call_on_dict, + ).new + )() +} + +#let test-ns() = { + let ns_test = namespace( + Foo: ns => class( + fields: (x: Int), + methods: ( + to_bar: self => ns("foo_to_bar")(self), + ), + ), + Bar: ns => class( + fields: (y: Int), + methods: ( + to_foo: self => (ns("Foo").new)(x: self.y), + ), + ), + foo_to_bar: ns => foo => { (ns("Bar").new)(y: foo.x) }, + ) + let foo = (ns_test("Foo").new)(x: 3) + assert.eq(foo.meta.repr, "(x: 3, to_bar: (..) => ..)") + assert.eq((foo.to_bar)().meta.repr, "(y: 3, to_foo: (..) => ..)") + + let bar = (ns_test("Bar").new)(y: 2) + assert.eq(bar.meta.repr, "(y: 2, to_foo: (..) => ..)") + assert.eq((bar.to_foo)().meta.repr, "(x: 2, to_bar: (..) => ..)") +} + +#let test-doc() = { + let ns = namespace( + foo: ns => { + let foo(x) = if x == 0 { "FOO" } else { ns("bar")(x - 1) } + foo + }, + bar: ns => { + let bar(x) = if x == 0 { "BAR" } else { ns("foo")(x - 1) } + bar + }, + ) + let foo = ns("foo") + assert.eq(foo(3), "BAR") + assert.eq(foo(4), "FOO") +} diff --git a/packages/preview/typsy/0.2.1/src/safe-counters.typ b/packages/preview/typsy/0.2.1/src/safe-counters.typ new file mode 100644 index 0000000000..cf117b0b42 --- /dev/null +++ b/packages/preview/typsy/0.2.1/src/safe-counters.typ @@ -0,0 +1,69 @@ +/// Like the built-in `counter`, but without risk of using the same string twice. +/// +/// Usage is +/// ```typst +/// #let counter1 = safe-counter(()=>{}) +/// #let counter2 = safe-counter(()=>{}) +/// // ... +/// ``` +/// This relies on the fact that every anonymous function is a unique object, and we can use this to build a unique key +/// for creating the counter. Note that you cannot wrap this into a `let safe-counter2() = {safe-counter(()=>{})}` +/// because we need it to be a *different* anonymous function each time. +/// +/// *Returns:* +/// +/// A `counter(...)`. +/// +/// *Arguments:* +/// +/// - symbol (function): this should be a `()=>{}`. +#let safe-counter(symbol) = { + counter(heading.where(level: symbol)) +} + +#let test-basic() = { + let c1 = safe-counter(() => {}) + let c2 = safe-counter(() => {}) + context { + assert.eq(c1.get().at(0), 0) + assert.eq(c2.get().at(0), 0) + } + c1.step() + context { + assert.eq(c1.get().at(0), 1) + assert.eq(c2.get().at(0), 0) + } + c2.step() + context { + assert.eq(c1.get().at(0), 1) + assert.eq(c2.get().at(0), 1) + } +} + +// This trick is used in `tree-counters`. +#let test-undocumented-sneaky-trick() = { + let symbol = ()=>{} + let c1 = safe-counter((1, symbol)) + let c2 = safe-counter((2, symbol)) + let c1-again = safe-counter((1, symbol)) + context { + assert.eq(c1.get().at(0), 0) + assert.eq(c2.get().at(0), 0) + assert.eq(c1-again.get().at(0), 0) + } + c1.step() + context { + assert.eq(c1.get().at(0), 1) + assert.eq(c2.get().at(0), 0) + assert.eq(c1-again.get().at(0), 1) + } + c2.step() + context { + assert.eq(c1.get().at(0), 1) + assert.eq(c2.get().at(0), 1) + assert.eq(c1-again.get().at(0), 1) + } +} + +#test-basic() +#test-undocumented-sneaky-trick() diff --git a/packages/preview/typsy/0.2.1/src/safe-state.typ b/packages/preview/typsy/0.2.1/src/safe-state.typ new file mode 100644 index 0000000000..140edc297a --- /dev/null +++ b/packages/preview/typsy/0.2.1/src/safe-state.typ @@ -0,0 +1,151 @@ +#import "./classes.typ": class +#import "./match.typ": Any, Function + +#let _underlying-safe-state = state("--typsy-sentinel-very-secret-state-string", ()) +#let _at(self, iterator) = { + // Yay, O(n) lookup! Oh well, I don't see a way around around this. Fortunate that Typst has ubiquitous caching. + for (key, val) in iterator { + if key == self.symbol { + return val + } + } + self.default +} +#let _state-cls = class( + fields: (symbol: Function, default: Any), + methods: ( + at: (self, loc) => { + _at(self, _underlying-safe-state.at(loc)) + }, + final: self => { + _at(self, _underlying-safe-state.final()) + }, + get: self => { + _at(self, _underlying-safe-state.get()) + }, + update: (self, value) => context { + let new = () + let found = false + for (key, val) in _underlying-safe-state.at(here()) { + if key == self.symbol { + new.push((key, value)) + found = true + } else { + new.push((key, val)) + } + } + if not found { + new.push((self.symbol, value)) + } + _underlying-safe-state.update(new) + }, + ), +) + +/// Like the built-in `state`, but without risk of using the same string twice. +/// +/// Usage is +/// ```typst +/// #let state1 = safe-state(()=>{}, "some default value") +/// #let state2 = safe-state(()=>{}, "another default value") +/// // ... +/// ``` +/// This relies on the fact that every anonymous function is a unique object, and we can use this to build a unique key +/// for creating the state. Note that you cannot wrap this into a `let safe-state2() = {safe-state(()=>{})}` because +/// we need it to be a *different* anonymous function each time. +/// +/// *Methods:* +/// +/// It provides the following of the usual `state` methods. In every case note that they must be surrounded with an +/// extra pair of brackets, due to Typst limitations. +/// +/// - `(self.at)(loc)`: as the usual `state.at`. +/// - `(self.final)()`: as the usual `state.final`. +/// - `(self.get)()`: as the usual `state.get`. +/// - `(self.update)(value)`: as the usual `state.update`. +/// +/// *Returns:* +/// +/// A `state(...)`. +/// +/// *Arguments:* +/// +/// - symbol (function): this should be a `()=>{}`. +/// - default (any): the default value of the state. +#let safe-state(symbol, default) = { (_state-cls.new)(symbol: symbol, default: default) } + + +#let test-safe-state() = context { + let x = safe-state(()=>{}, 1) + let y = safe-state(()=>{}, "hello") + let loc = here() + context { + assert.eq((x.final)(), 9) + assert.eq((y.final)(), selector(heading)) + assert.eq((x.get)(), 1) + assert.eq((y.get)(), "hello") + } + + (x.update)("foo") + context { + assert.eq((x.get)(), "foo") + assert.eq((y.get)(), "hello") + } + (y.update)(none) + context { + assert.eq((x.get)(), "foo") + assert.eq((y.get)(), none) + } + + (x.update)(9) + context { + assert.eq((x.get)(), 9) + assert.eq((y.get)(), none) + } + (y.update)(selector(heading)) + context { + assert.eq((x.at)(loc), 1) + assert.eq((y.at)(loc), "hello") + assert.eq((x.get)(), 9) + assert.eq((y.get)(), selector(heading)) + } +} +#test-safe-state() + + +#let test-regular-state-for-comparison() = context { + let x = state("--typsy-test-sentinel-string1", 1) + let y = state("--typsy-test-sentinel-string2", "hello") + let loc = here() + context { + assert.eq(x.final(), 9) + assert.eq(y.final(), selector(heading)) + assert.eq(x.get(), 1) + assert.eq(y.get(), "hello") + } + + x.update("foo") + context { + assert.eq(x.get(), "foo") + assert.eq(y.get(), "hello") + } + y.update(none) + context { + assert.eq(x.get(), "foo") + assert.eq(y.get(), none) + } + + x.update(9) + context { + assert.eq(x.get(), 9) + assert.eq(y.get(), none) + } + y.update(selector(heading)) + context { + assert.eq(x.at(loc), 1) + assert.eq(y.at(loc), "hello") + assert.eq(x.get(), 9) + assert.eq(y.get(), selector(heading)) + } +} +#test-regular-state-for-comparison() diff --git a/packages/preview/typsy/0.2.1/src/tree-counters.typ b/packages/preview/typsy/0.2.1/src/tree-counters.typ new file mode 100644 index 0000000000..3e808f9bd4 --- /dev/null +++ b/packages/preview/typsy/0.2.1/src/tree-counters.typ @@ -0,0 +1,714 @@ +#import "./classes.typ": class +#import "./format.typ": fmt, panic-fmt +#import "./match.typ": Arguments, Counter, Function, Int, Literal, Str, Union, matches +#import "./safe-counters.typ": safe-counter +#import "./typecheck.typ": typecheck + +/// - value (int, function) +/// - default (none, int) +#let _check_update(value, default) = { + if type(value) == function { + if default != none { + value = x => value(x + default) - default + } + value = typecheck(Arguments(Int), Int, value, invalid-return: (_, _) => error_msg) + } else if type(value) == int { + if default != none { + value = value - default + } + } else { + panic-fmt("`The `value` in `(counter.update)(value)` must either be an `int`, or a function `int -> int`.") + } + value +} + +/// - numbers (array<..int>) +/// - default (none, int) +#let _offset(numbers, default) = { + if default == none { + numbers + } else { + assert.eq(numbers.len(), 1) + (numbers.at(0) + default,) + } +} + + +#let TreeCounter = class( + name: "TreeCounter", + fields: ( + display: Function, /*self->str*/ + get: Function, /*self->array*/ + step: Function, /*self->content*/ + update: Function, /*(self, union int>>)->content*/ + ), + methods: ( + subcounter: (self, symbol /*function*/, numbering: ".1", default: 0) => /*Counter*/ { + let get_raw_counter() = { + let parent_index = (self.get)().map(str).join(".") + // This is a sneaky undocumented (but tested) usage of `safe-counter`. + safe-counter((parent_index, symbol)) + } + let display(_) = { + (self.display)() + if numbering == "" {""} else {get_raw_counter().display(numbering)} + } + let get(_) = (self.get)() + _offset(get_raw_counter().get(), default) + // `step` needs `context` as it needs to resolve its parent counter via `parent.get()`. + let step(_) = context get_raw_counter().step() + let update(_, value) = context get_raw_counter().update(_check_update(value, default)) + (self.meta.cls.new)(display: display, get: get, step: step, update: update) + }, + take: self => /*content*/ { + (self.step)() + context (self.display)() + }, + ), +) + +/// - raw_counter (counter): +/// - level (int): +/// - get_numbering (function): ()->union(str,function) +/// - default (none, int): default value for the counter. +#let _tree-counter(raw_counter, level, get_numbering, default) = { + let display = self => { + let n = get_numbering() + if n == "" { + "" + } else { + numbering(n, ..(self.get)()) + } + } + let get(self) = { + let raw_get = _offset(raw_counter.get(), default) + let diff = level - raw_get.len() + if diff <= 0 { + raw_get.slice(0, level) + } else { + raw_get + (0,) * diff + } + } + let step = self => raw_counter.step(level: level) + let update(self, value) = { + raw_counter.update(_check_update(value, default)) + } + (TreeCounter.new)(display: display, get: get, step: step, update: update) +} + +#let _get-numbering(named, x) = { + () => named.at("numbering", default: if x == none or x.numbering == none { "1.1" } else { x.numbering }) +} + +/// Creates the root node of a tree-of-counters. Subcounters are then created using its `.subcounter` method. +/// +/// *Example:* +/// +/// ```typst +/// #let heading-counter = tree-counter(heading, level: 1) +/// #let theorem-counter = (heading-counter.subcounter)(()=>{}, numbering: ".A") +/// #(heading-counter.step)() +/// #(theorem-counter.step)() +/// #(theorem-counter.step)() +/// #context assert.eq((theorem-counter.display)(), "1.B") +/// ``` +/// +/// *Constructors:* +/// +/// This can be called in a few distinct ways, corresponding to the following call signatures: +/// +/// 1. To create a brand-new counter: `tree-counter(()=>{})` +/// - with non-zero default: `tree-counter(()=>{}, default: 3)`. +/// 2. Track an existing counter at some level: `tree-counter(some-counter)` +/// - with non-one level: `tree-counter(some-counter, level: 2)`. +/// 3. To track the `heading` counter: `tree-counter(heading)`. +/// - with non-one level: `tree-counter(heading, level: 2)`. +/// 4. To track the `page` counter: `tree-counter(page)`. +/// 5. To track the `figure` counter: `tree-counter(figure)`. +/// +/// In all cases then `numbering` can be passed, e.g. `tree-counter(()=>{}, numbering: "A.")`, to use a non-default +/// numbering. +/// +/// Notes: +/// +/// - When creating a brand-new counter, then the first argument should be specifically the anonymous function +/// `()=>{}`. Like with `safe-counter`, we use the distinctness of all anonymous functions to assign a unique +/// identity to our counter. +/// - Wrapping an existing counter, or the `heading` counter, requires `level` as a mandatory keyword argument. This is +/// because tree-counters do not have a notion of `level`. Just create subcounters instead! Correspondingly when +/// wrapping an existing counter, then we must provide the level of that counter that we wish to track. (This does +/// not apply to the `page` counter, which seems to be special and is always a level-1 counter.) +/// - The `numbering` arguments should be a `str` or `function`, as per what can be passed to the builtin +/// `numbering(...)` function. This is provided at initialisation time, rather than `display` time, so that child +/// counters are not required to specify how all their parents should be `display`ed. (And we respect the existing +/// `heading.numbering` or `page.numbering` if tracking those counters.) +/// +/// Examples: +/// +/// 1. `tree-counter(()=>{})` to create a brand new counter. +/// 2. `tree-counter(counter("foo"), level: 1)` to track an existing counter. +/// 3. `tree-counter(heading, level: 2)` to track the heading counter (at level 2). +/// +/// *Methods:* +/// +/// It provides the following of the usual `counter` methods. In every case note that they must be surrounded with an +/// extra pair of brackets, due to Typst limitations. +/// +/// - `(self.display)()`: as the usual `counter.display`. +/// Note that this does not take a `numbering` argument: provide this at initialisation time instead. (This is so +/// that each node in the tree can specify how it should be displayed without requiring all children to do so.) +/// - `(self.get)()`: as the usual `counter.get`. +/// - `(self.step)()`: as the usual `counter.step`. +/// Note that this does not take a `level` argument. Create a `.subcounter` instead. +/// - `(self.subcounter)(()=>{}, numbering: ".1", default : 0)`: the star of the show! This creates a subcounter. The +/// first argument should be specifically `()=>{}`, in the same way as described for the main constructor. The +/// numbering is optional and used when `.display`ing the subcounter. The default is the initial value of the +/// counter. +/// - `(self.take)()`: this is a useful shorthand for `(self.step)()` followed by `(self.display)()`. +/// - `(self.update)(value)`: as the usual `counter.update`. +/// +/// We intentionally do not provide `.at` or `.final` methods, as these are very difficult to reason about when using +/// trees of counters. +/// +/// *Returns:* +/// +/// A `TreeCounter` object. +/// +/// *Arguments:* +/// +/// As described in "Constructors' above. +#let tree-counter(..args) = { + let pos = args.pos() + let named = args.named() + let heading_matches = Union( + Arguments(Literal(heading), numbering: Union(Str, Function), level: Int), + Arguments(Literal(heading), numbering: Union(Str, Function)), + Arguments(Literal(heading), level: Int), + Arguments(Literal(heading)), + ) + let page_matches = Union( + Arguments(Literal(page), numbering: Union(Str, Function)), + Arguments(Literal(page)), + ) + let figure_matches = Union( + Arguments(Literal(figure), numbering: Union(Str, Function)), + Arguments(Literal(figure)), + ) + let new_matches = Union( + Arguments(Function, numbering: Union(Str, Function), default: Int), + Arguments(Function, numbering: Union(Str, Function)), + Arguments(Function, default: Int), + Arguments(Function), + ) + let existing_matches = Union( + Arguments(Counter, numbering: Union(Str, Function), level: Int), + Arguments(Counter, numbering: Union(Str, Function)), + Arguments(Counter, level: Int), + Arguments(Counter), + ) + + if matches(heading_matches, args) { + // Track heading counter + let get-numbering = _get-numbering(named, heading) + _tree-counter(counter(heading), named.at("level", default: 1), get-numbering, none) + } else if matches(page_matches, args) { + // Track page counter + let get-numbering = _get-numbering(named, page) + _tree-counter(counter(page), 1, get-numbering, none) + } else if matches(figure_matches, args) { + // Track figure counter + let get-numbering = _get-numbering(named, figure) + _tree-counter(counter(figure), 1, get-numbering, none) + } else if matches(new_matches, args) { + // New counter + let (symbol,) = pos + let get-numbering = _get-numbering(named, none) + let default = named.at("default", default: 0) + _tree-counter(safe-counter(symbol), 1, get-numbering, default) + } else if matches(existing_matches, args) { + // Track existing counter + let (count,) = pos + let get-numbering = _get-numbering(named, none) + _tree-counter(count, named.at("level", default: 1), get-numbering, none) + } else { + panic-fmt( + "Received invalid arguments `{}` that did not match any call signature for `tree-counter`.", + repr(args), + ) + } +} + +#let test-new-counter() = { + let root = tree-counter(() => {}) + context assert.eq((root.get)(), (0,)) + context assert.eq((root.display)(), "0") + (root.step)() + context assert.eq((root.get)(), (1,)) + context assert.eq((root.display)(), "1") + (root.step)() + context assert.eq((root.get)(), (2,)) + context assert.eq((root.display)(), "2") + + let subcounter1 = (root.subcounter)(() => {}) + context assert.eq((subcounter1.get)(), (2, 0)) + context assert.eq((subcounter1.display)(), "2.0") + (subcounter1.step)() + context assert.eq((subcounter1.get)(), (2, 1)) + context assert.eq((subcounter1.display)(), "2.1") + (root.step)() + context assert.eq((subcounter1.get)(), (3, 0)) + context assert.eq((subcounter1.display)(), "3.0") + + let subcounter2 = (root.subcounter)(() => {}, numbering: ".A") + context assert.eq((subcounter2.get)(), (3, 0)) + context assert.eq((subcounter2.display)(), "3.-") + (subcounter2.step)() + context assert.eq((subcounter2.get)(), (3, 1)) + context assert.eq((subcounter2.display)(), "3.A") + (root.step)() + context assert.eq((subcounter2.get)(), (4, 0)) + context assert.eq((subcounter2.display)(), "4.-") + + (root.update)(9) + context assert.eq((root.get)(), (9,)) + context assert.eq((subcounter2.get)(), (9, 0)) + context (subcounter2.update)(4) + context assert.eq((subcounter2.get)(), (9, 4)) + (root.update)(x => x + 10) + context assert.eq((root.get)(), (19,)) + context assert.eq((subcounter2.get)(), (19, 0)) + (subcounter2.update)(x => x + 10) + context assert.eq((subcounter2.get)(), (19, 10)) +} + +#let test-new-counter-with-numbering() = { + let root = tree-counter(() => {}, numbering: "A.") + context assert.eq((root.display)(), "-.") + (root.step)() + context assert.eq((root.get)(), (1,)) + context assert.eq((root.display)(), "A.") + (root.step)() + context assert.eq((root.get)(), (2,)) + context assert.eq((root.display)(), "B.") + + let subcounter1 = (root.subcounter)(() => {}) + context assert.eq((subcounter1.get)(), (2, 0)) + context assert.eq((subcounter1.display)(), "B..0") + (subcounter1.step)() + context assert.eq((subcounter1.get)(), (2, 1)) + context assert.eq((subcounter1.display)(), "B..1") + (root.step)() + context assert.eq((subcounter1.get)(), (3, 0)) + context assert.eq((subcounter1.display)(), "C..0") + + let subcounter2 = (root.subcounter)(() => {}, numbering: "A") + context assert.eq((subcounter2.get)(), (3, 0)) + context assert.eq((subcounter2.display)(), "C.-") + (subcounter2.step)() + context assert.eq((subcounter2.get)(), (3, 1)) + context assert.eq((subcounter2.display)(), "C.A") + (root.step)() + context assert.eq((subcounter2.get)(), (4, 0)) + context assert.eq((subcounter2.display)(), "D.-") + + (root.update)(9) + context assert.eq((root.get)(), (9,)) + context assert.eq((subcounter2.get)(), (9, 0)) + context (subcounter2.update)(4) + context assert.eq((subcounter2.get)(), (9, 4)) + (root.update)(x => x + 10) + context assert.eq((root.get)(), (19,)) + context assert.eq((subcounter2.get)(), (19, 0)) + (subcounter2.update)(x => x + 10) + context assert.eq((subcounter2.get)(), (19, 10)) +} + +#let test-wrap-counter() = { + let root = tree-counter(safe-counter(() => {}), level: 2) + context assert.eq((root.get)(), (0, 0)) + context assert.eq((root.display)(), "0.0") + (root.step)() + context assert.eq((root.get)(), (0, 1)) + context assert.eq((root.display)(), "0.1") + (root.step)() + context assert.eq((root.get)(), (0, 2)) + context assert.eq((root.display)(), "0.2") + + let subcounter1 = (root.subcounter)(() => {}) + context assert.eq((subcounter1.get)(), (0, 2, 0)) + context assert.eq((subcounter1.display)(), "0.2.0") + (subcounter1.step)() + context assert.eq((subcounter1.get)(), (0, 2, 1)) + context assert.eq((subcounter1.display)(), "0.2.1") + (root.step)() + context assert.eq((subcounter1.get)(), (0, 3, 0)) + context assert.eq((subcounter1.display)(), "0.3.0") + + let subcounter2 = (root.subcounter)(() => {}, numbering: ".A") + context assert.eq((subcounter2.get)(), (0, 3, 0)) + context assert.eq((subcounter2.display)(), "0.3.-") + (subcounter2.step)() + context assert.eq((subcounter2.get)(), (0, 3, 1)) + context assert.eq((subcounter2.display)(), "0.3.A") + (root.step)() + context assert.eq((subcounter2.get)(), (0, 4, 0)) + context assert.eq((subcounter2.display)(), "0.4.-") + + (root.update)(9) + context assert.eq((root.get)(), (9, 0)) + context assert.eq((subcounter2.get)(), (9, 0, 0)) + context (subcounter2.update)(4) + context assert.eq((subcounter2.get)(), (9, 0, 4)) + (root.update)(x => x + 10) + context assert.eq((root.get)(), (19, 0)) + context assert.eq((subcounter2.get)(), (19, 0, 0)) + (subcounter2.update)(x => x + 10) + context assert.eq((subcounter2.get)(), (19, 0, 10)) +} + +#let test-wrap-counter-with-numbering() = { + let root = tree-counter(safe-counter(() => {}), numbering: "1.", level: 2) + context assert.eq((root.get)(), (0, 0)) + context assert.eq((root.display)(), "0.0.") + (root.step)() + context assert.eq((root.get)(), (0, 1)) + context assert.eq((root.display)(), "0.1.") + (root.step)() + context assert.eq((root.get)(), (0, 2)) + context assert.eq((root.display)(), "0.2.") + + let subcounter1 = (root.subcounter)(() => {}) + context assert.eq((subcounter1.get)(), (0, 2, 0)) + context assert.eq((subcounter1.display)(), "0.2..0") + (subcounter1.step)() + context assert.eq((subcounter1.get)(), (0, 2, 1)) + context assert.eq((subcounter1.display)(), "0.2..1") + (root.step)() + context assert.eq((subcounter1.get)(), (0, 3, 0)) + context assert.eq((subcounter1.display)(), "0.3..0") + + let subcounter2 = (root.subcounter)(() => {}, numbering: "A") + context assert.eq((subcounter2.get)(), (0, 3, 0)) + context assert.eq((subcounter2.display)(), "0.3.-") + (subcounter2.step)() + context assert.eq((subcounter2.get)(), (0, 3, 1)) + context assert.eq((subcounter2.display)(), "0.3.A") + (root.step)() + context assert.eq((subcounter2.get)(), (0, 4, 0)) + context assert.eq((subcounter2.display)(), "0.4.-") + + (root.update)(9) + context assert.eq((root.get)(), (9, 0)) + context assert.eq((subcounter2.get)(), (9, 0, 0)) + context (subcounter2.update)(4) + context assert.eq((subcounter2.get)(), (9, 0, 4)) + (root.update)(x => x + 10) + context assert.eq((root.get)(), (19, 0)) + context assert.eq((subcounter2.get)(), (19, 0, 0)) + (subcounter2.update)(x => x + 10) + context assert.eq((subcounter2.get)(), (19, 0, 10)) +} + +#let test-heading-counter() = { + counter(heading).update(0) + // Test that we match the default numbering (when `heading.numbering = none`). + context assert.eq(counter(heading).display(), "0") + counter(heading).step(level: 2) + context assert.eq(counter(heading).display(), "0.1") + let root = tree-counter(heading, level: 3) + context assert.eq((root.get)(), (0, 1, 0)) + context assert.eq((root.display)(), "0.1.0") + (root.step)() + context assert.eq((root.get)(), (0, 1, 1)) + context assert.eq((root.display)(), "0.1.1") + (root.step)() + // Test that we track any changes to `heading.numbering`. + set heading(numbering: "1.A.") + context assert.eq((root.get)(), (0, 1, 2)) + context assert.eq((root.display)(), "0.A.B.") + // Test that we track exogeneous changes to the counter. + counter(heading).step(level: 1) + context assert.eq((root.get)(), (1, 0, 0)) + context assert.eq((root.display)(), "1.-.-.") + + let subcounter1 = (root.subcounter)(() => {}) + context assert.eq((subcounter1.get)(), (1, 0, 0, 0)) + context assert.eq((subcounter1.display)(), "1.-.-..0") + (subcounter1.step)() + context assert.eq((subcounter1.get)(), (1, 0, 0, 1)) + context assert.eq((subcounter1.display)(), "1.-.-..1") + (root.step)() + context assert.eq((subcounter1.get)(), (1, 0, 1, 0)) + context assert.eq((subcounter1.display)(), "1.-.A..0") + + let subcounter2 = (root.subcounter)(() => {}, numbering: ".A") + context assert.eq((subcounter2.get)(), (1, 0, 1, 0)) + context assert.eq((subcounter2.display)(), "1.-.A..-") + (subcounter2.step)() + context assert.eq((subcounter2.get)(), (1, 0, 1, 1)) + context assert.eq((subcounter2.display)(), "1.-.A..A") + (root.step)() + context assert.eq((subcounter2.get)(), (1, 0, 2, 0)) + context assert.eq((subcounter2.display)(), "1.-.B..-") + + // Test that we track any changes to `heading.numbering`. + set heading(numbering: "1.1") + context assert.eq((subcounter2.display)(), "1.0.2.-") + + // Test that we track exogeneous changes to the counter. + counter(heading).step(level: 1) + context assert.eq((subcounter2.display)(), "2.0.0.-") + + (root.update)(9) + context assert.eq((root.get)(), (9, 0, 0)) + context assert.eq((subcounter2.get)(), (9, 0, 0, 0)) + (subcounter2.update)(4) + context assert.eq((subcounter2.get)(), (9, 0, 0, 4)) + (root.update)(x => x + 10) + context assert.eq((root.get)(), (19, 0, 0)) + context assert.eq((subcounter2.get)(), (19, 0, 0, 0)) + (subcounter2.update)(x => x + 10) + context assert.eq((subcounter2.get)(), (19, 0, 0, 10)) +} + +#let test-heading-counter-without-level() = { + counter(heading).update(0) + let root = tree-counter(heading) + context assert.eq((root.get)(), (0, 1, 0)) + context assert.eq((root.display)(), "0.1.0") + (root.step)() + context assert.eq((root.get)(), (0, 1, 1)) + context assert.eq((root.display)(), "0.1.1") + (root.step)() + + (root.update)(9) + context assert.eq((root.get)(), (9,)) + (root.update)(x => x + 10) + context assert.eq((root.get)(), (19,)) +} + +#let test-page-counter() = { + counter(page).update(1) + // Test that we match the default numbering (when `page.numbering = none`). + context assert.eq(counter(page).display(), "1") + counter(page).step() + context assert.eq(counter(page).display(), "2") + let root = tree-counter(page) + context assert.eq((root.get)(), (2,)) + context assert.eq((root.display)(), "2") + (root.step)() + context assert.eq((root.get)(), (3,)) + context assert.eq((root.display)(), "3") + (root.step)() + // Test that we track any changes to `page.numbering`. + set page(numbering: "A.") + context assert.eq((root.get)(), (4,)) + context assert.eq((root.display)(), "D.") + + let subcounter1 = (root.subcounter)(() => {}) + context assert.eq((subcounter1.get)(), (4, 0)) + context assert.eq((subcounter1.display)(), "D..0") + (subcounter1.step)() + context assert.eq((subcounter1.get)(), (4, 1)) + context assert.eq((subcounter1.display)(), "D..1") + (root.step)() + context assert.eq((subcounter1.get)(), (5, 0)) + context assert.eq((subcounter1.display)(), "E..0") + + let subcounter2 = (root.subcounter)(() => {}, numbering: "A") + context assert.eq((subcounter2.get)(), (5, 0)) + context assert.eq((subcounter2.display)(), "E.-") + (subcounter2.step)() + context assert.eq((subcounter2.get)(), (5, 1)) + context assert.eq((subcounter2.display)(), "E.A") + (root.step)() + context assert.eq((subcounter2.get)(), (6, 0)) + context assert.eq((subcounter2.display)(), "F.-") + + // Test that we track any changes to `heading.numbering`. + set page(numbering: "1") + context assert.eq((subcounter2.display)(), "6-") + + (root.update)(9) + context assert.eq((root.get)(), (9,)) + context assert.eq((subcounter2.get)(), (9, 0)) + context (subcounter2.update)(4) + context assert.eq((subcounter2.get)(), (9, 4)) + (root.update)(x => x + 10) + context assert.eq((root.get)(), (19,)) + context assert.eq((subcounter2.get)(), (19, 0)) + (subcounter2.update)(x => x + 10) + context assert.eq((subcounter2.get)(), (19, 10)) +} + +#let test-figure-counter() = { + counter(figure).update(1) + // Test that we match the default numbering (when `figure.numbering = none`). + context assert.eq(counter(figure).display(), "1") + counter(figure).step() + context assert.eq(counter(figure).display(), "2") + let root = tree-counter(figure) + context assert.eq((root.get)(), (2,)) + context assert.eq((root.display)(), "2") + (root.step)() + context assert.eq((root.get)(), (3,)) + context assert.eq((root.display)(), "3") + (root.step)() + // Test that we track any changes to `figure.numbering`. + set figure(numbering: "A.") + context assert.eq((root.get)(), (4,)) + context assert.eq((root.display)(), "D.") + + let subcounter1 = (root.subcounter)(() => {}) + context assert.eq((subcounter1.get)(), (4, 0)) + context assert.eq((subcounter1.display)(), "D..0") + (subcounter1.step)() + context assert.eq((subcounter1.get)(), (4, 1)) + context assert.eq((subcounter1.display)(), "D..1") + (root.step)() + context assert.eq((subcounter1.get)(), (5, 0)) + context assert.eq((subcounter1.display)(), "E..0") + + let subcounter2 = (root.subcounter)(() => {}, numbering: "A") + context assert.eq((subcounter2.get)(), (5, 0)) + context assert.eq((subcounter2.display)(), "E.-") + (subcounter2.step)() + context assert.eq((subcounter2.get)(), (5, 1)) + context assert.eq((subcounter2.display)(), "E.A") + (root.step)() + context assert.eq((subcounter2.get)(), (6, 0)) + context assert.eq((subcounter2.display)(), "F.-") + + // Test that we track any changes to `heading.numbering`. + set figure(numbering: "1") + context assert.eq((subcounter2.display)(), "6-") + + (root.update)(9) + context assert.eq((root.get)(), (9,)) + context assert.eq((subcounter2.get)(), (9, 0)) + context (subcounter2.update)(4) + context assert.eq((subcounter2.get)(), (9, 4)) + (root.update)(x => x + 10) + context assert.eq((root.get)(), (19,)) + context assert.eq((subcounter2.get)(), (19, 0)) + (subcounter2.update)(x => x + 10) + context assert.eq((subcounter2.get)(), (19, 10)) +} + +#let test-empty-numbering() = { + let root = tree-counter(()=>{}, numbering: "") + let sub1 = (root.subcounter)(()=>{}, numbering: "(a)") + context assert.eq((sub1.display)(), "(-)") + (sub1.step)() + context assert.eq((sub1.display)(), "(a)") +} + +#let test-sub-subcounter() = { + let root = tree-counter(() => {}) + let sub1 = (root.subcounter)(() => {}) + let sub2 = (sub1.subcounter)(() => {}) + context assert.eq((sub2.get)(), (0, 0, 0)) + context assert.eq((sub2.display)(), "0.0.0") + (root.step)() + context assert.eq((sub2.display)(), "1.0.0") + (sub2.step)() + context assert.eq((sub2.display)(), "1.0.1") + (sub1.step)() + context assert.eq((sub2.display)(), "1.1.0") + + (root.update)(9) + context assert.eq((root.get)(), (9,)) + context assert.eq((sub2.get)(), (9, 0, 0)) + context (sub2.update)(4) + context assert.eq((sub2.get)(), (9, 0, 4)) + (root.update)(x => x + 10) + context assert.eq((root.get)(), (19,)) + context assert.eq((sub2.get)(), (19, 0, 0)) + (sub2.update)(x => x + 10) + context assert.eq((sub2.get)(), (19, 0, 10)) +} + +#let test-default() = { + let root = tree-counter(()=>{}, default: 3) + context assert.eq((root.get)(), (3,)) + (root.step)() + context assert.eq((root.get)(), (4,)) + (root.step)() + context assert.eq((root.get)(), (5,)) + (root.update)(7) + context assert.eq((root.get)(), (7,)) + (root.update)(12) + context assert.eq((root.get)(), (12,)) + (root.step)() + context assert.eq((root.get)(), (13,)) + (root.update)(x=>calc.pow(x, 2)) + context assert.eq((root.get)(), (169,)) + (root.step)() + context assert.eq((root.get)(), (170,)) + + let s = (root.subcounter)(()=>{}, default: -3) + context assert.eq((s.get)(), (170, -3)) + (s.step)() + context assert.eq((s.get)(), (170, -2)) + (s.step)() + context assert.eq((s.get)(), (170, -1)) + (s.update)(5) + context assert.eq((s.get)(), (170, 5)) + (s.update)(x=>calc.pow(x, 2)) + context assert.eq((s.get)(), (170, 25)) + (root.step)() + context assert.eq((s.get)(), (171, -3)) + (s.update)(x=>calc.pow(x, 2)) + context assert.eq((s.get)(), (171, 9)) + + let ss = (s.subcounter)(()=>{}, default: 4) + context assert.eq((ss.get)(), (171, 9, 4)) + (ss.step)() + context assert.eq((ss.get)(), (171, 9, 5)) + (s.step)() + context assert.eq((ss.get)(), (171, 10, 4)) + (ss.update)(x=>calc.pow(x, 2)) + context assert.eq((ss.get)(), (171, 10, 16)) + +} + +#let test-doc() = { + counter(heading).update(0) + let heading-counter = tree-counter(heading, level: 1) + let theorem-counter = (heading-counter.subcounter)(() => {}, numbering: ".A") + (heading-counter.step)() + (theorem-counter.step)() + (theorem-counter.step)() + context assert.eq((theorem-counter.display)(), "1.B") +} + +#let panic-on-bad-new-counter() = { + tree-counter(3) +} + +#let panic-on-bad-new-counter-with-numbering() = { + tree-counter(3, numbering: "A.") +} + +#let panic-on-bad-wrap-counter() = { + tree-counter(3, level: 1) +} + +#let panic-on-bad-wrap-counter-with-numbering() = { + tree-counter(3, level: 1, numbering: "A.") +} + +#let panic-on-bad-keywords() = { + tree-counter(() => {}, foo: 3) +} + +// Need to include these as they include `context` asserts: https://github.com/Myriad-Dreamin/tinymist/issues/2128 +#test-new-counter() +#test-new-counter-with-numbering() +#test-wrap-counter() +#test-wrap-counter-with-numbering() +#test-heading-counter() +#test-page-counter() +#test-figure-counter() +#test-empty-numbering() +#test-sub-subcounter() +#test-default() +#test-doc() diff --git a/packages/preview/typsy/0.2.1/src/typecheck.typ b/packages/preview/typsy/0.2.1/src/typecheck.typ new file mode 100644 index 0000000000..509c0dd908 --- /dev/null +++ b/packages/preview/typsy/0.2.1/src/typecheck.typ @@ -0,0 +1,62 @@ +#import "./match.typ": Arguments, Int, matches +#import "./format.typ": fmt + +/// Wraps a function with runtime typechecking. +/// +/// *Example:* +/// +/// ```typst +/// #let add_integers = typecheck(Arguments(Int, Int), Int, (x, y) => x + y) +/// #let five = add_integers(2, 3) +/// #let will_panic = add_integers("hello ", "world") +/// ``` +/// +/// *Returns:* +/// +/// A wrapped version of `fn`, that consumes the same arguments and returns the same output, but which will panic if the +/// arguments do not match `args-type` or if the return value does not match `return-type`. +/// +/// *Arguments:* +/// +/// - args-type (Pattern): typically an `Arguments(...)` object specifying the expected arguments. Can in general be any +/// pattern, e.g. `Any` to accept anything, `Union(Arguments(...), Arguments(...))` to accept multiple arguments, +/// etc. +/// - return-type (Pattern): the pattern to check the return value of `fn` against. +/// - fn (function): the function to wrap. +/// - invalid-args (function): a function `args -> str` formatting the error message for invalid arguments. +/// - invalid-return (function): a function `(args, returnval) -> str` formatting the error message for an invalid +/// return value. +#let typecheck( + args-type, + return-type, + fn, + invalid-args: args => fmt("Arguments `{}` do not match type annotation.", repr(args)), + invalid-return: (args, returnval) => fmt("Return value `{}` does not match type annotation.", repr(returnval)), +) = { + (..args) => { + if not matches(args-type, args) { + panic(invalid-args(args)) + } + let returnval = fn(..args) + if not matches(return-type, returnval) { + panic(invalid-return(args, returnval)) + } + returnval + } +} + + +#let _add_integers = typecheck(Arguments(Int, Int), Int, (x, y) => x + y) +#let test-typecheck() = { + let five = _add_integers(2, 3) + assert.eq(five, 5) +} + +#let panic-on-typecheck-args() = { + _add_integers("hello ", "world") +} + +#let _add_integers_buggy = typecheck(Arguments(Int, Int), Int, (x, y) => repr(x + y)) +#let panic-on-typecheck-return() = { + _add_integers_buggy(2, 3) +} diff --git a/packages/preview/typsy/0.2.1/typst.toml b/packages/preview/typsy/0.2.1/typst.toml new file mode 100644 index 0000000000..0fc214b225 --- /dev/null +++ b/packages/preview/typsy/0.2.1/typst.toml @@ -0,0 +1,10 @@ +[package] +name = "typsy" +version = "0.2.1" +entrypoint = "src/lib.typ" +authors = ["Patrick Kidger "] +license = "Apache-2.0" +description = "Classes/structs, pattern matching, safe counters... and more! Your one-stop library for programming tools not already in core Typst." +repository = "https://github.com/patrick-kidger/typsy" +keywords = ["class", "struct", "pattern", "match", "counter", "namespace", "functional", "typecheck", "type", "enum", "enumeration", "variant", "trait", "abstract", "programmatic", "programming", "scripting"] +categories = ["scripting"]