Commitb4e0ac51Recorded12 Jan 2026Repositorysigil-version
sigil-version: Move (sigil version) to dedicated package
Message
The version module provides semver parsing, comparison, and constraints. It's only used by build/release tooling (sigil-build, sigil-changes), not general applications, so it doesn't belong in stdlib.
Changes: - Create packages/sigil-version with the module - Add sigil-version dependency to sigil-build and sigil-changes - Update Makefile: add VERSIONPKG, remove SQLITEPKG (not needed for bootstrap) - Fix stdlib-modules list in sigil-publish docs (add missing modules: array, dict, exceptions, fn, list, profile, random, seq) - Add API review in notes/review/sigil-version.md
Changed
package.sgl | 15 ++++++
src/sigil/version.sgl | 470 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 485 insertions(+)Diff
package.sgladded
@@ -0,0 +1,15 @@
+1
;;; sigil-version - Semantic Versioning+2
;;;+3
;;; Parse, compare, and check version constraints using semantic versioning+4
;;; (semver 2.0.0). Useful for package management and compatibility checks.+5
+6
(package+7
name: "sigil-version"+8
version: "0.1.0"+9
description: "Semantic versioning parsing and comparison"+10
url: "https://codeberg.org/sigil/sigil"+11
license: "BSD-3-Clause"+12
authors: (list "David Wilson <[email protected]>")+13
+14
dependencies: (list+15
(from-workspace name: "sigil-stdlib")))src/sigil/version.sgladded
@@ -0,0 +1,470 @@
+1
;;; (sigil version) - Semantic Versioning+2
;;;+3
;;; Parse, compare, and check version constraints using semantic versioning+4
;;; (semver 2.0.0). Useful for package management and compatibility checks.+5
;;;+6
;;; ```scheme+7
;;; (import (sigil version))+8
;;;+9
;;; (define v (parse-version "1.2.3-alpha.1"))+10
;;; (version-major v) ; => 1+11
;;; (version<? v (parse-version "1.2.4")) ; => #t+12
;;;+13
;;; ;; Check version constraints+14
;;; (version-satisfies? v ">= 1.0.0") ; => #t+15
;;; (version-satisfies? v "^1.2.0") ; => #t (compatible)+16
;;; ```+17
;;;+18
;;; ## Version Format+19
;;;+20
;;; Versions follow the pattern: `MAJOR.MINOR.PATCH[-PRERELEASE][+BUILD]`+21
;;;+22
;;; - **MAJOR**: Incremented for breaking changes+23
;;; - **MINOR**: Incremented for new features (backwards compatible)+24
;;; - **PATCH**: Incremented for bug fixes+25
;;; - **PRERELEASE**: Optional pre-release identifier (e.g., `alpha.1`)+26
;;; - **BUILD**: Optional build metadata (ignored in comparisons)+27
;;;+28
;;; ## Constraint Operators+29
;;;+30
;;; | Operator | Example | Meaning |+31
;;; |----------|---------|---------|+32
;;; | (none) | `1.2.3` | Exact version |+33
;;; | `=` | `= 1.2.3` | Exact version |+34
;;; | `>` | `> 1.2.3` | Greater than |+35
;;; | `>=` | `>= 1.2.3` | Greater than or equal |+36
;;; | `<` | `< 1.2.3` | Less than |+37
;;; | `<=` | `<= 1.2.3` | Less than or equal |+38
;;; | `^` | `^1.2.3` | Compatible (same major, >= given) |+39
;;; | `~` | `~1.2.3` | Approximately (same major.minor, >= given) |+40
;;; | `*` | `*` | Any version |+41
+42
(define-library (sigil version)+43
(import (sigil string) ; For string<?, string>?, string-trim+44
(sigil struct)) ; For define-struct+45
+46
(export+47
;; Version record+48
version version?+49
version-major version-minor version-patch+50
version-prerelease version-build+51
version->string+52
+53
;; Parsing+54
parse-version+55
string->version+56
+57
;; Comparison+58
version=?+59
version<?+60
version>?+61
version<=?+62
version>=?+63
version-compare+64
+65
;; Constraints+66
parse-constraint+67
version-satisfies?+68
constraint-satisfies?+69
+70
;; Utilities+71
version-compatible?+72
version-bump-major+73
version-bump-minor+74
version-bump-patch)+75
+76
(begin+77
+78
;; ============================================================+79
;; Version Record+80
;; ============================================================+81
+82
;;; A semantic version with major, minor, patch, prerelease, and build components.+83
;;;+84
;;; ```scheme+85
;;; (version major: 1 minor: 2 patch: 3)+86
;;; (version major: 2 minor: 0 patch: 0 prerelease: '("alpha" 1))+87
;;; (version major: 1 minor: 0 patch: 0 build: "20240101")+88
;;; ```+89
(define-struct version+90
;;; Major version number. Breaking changes increment this.+91
(major default: 0)+92
;;; Minor version number. New features increment this.+93
(minor default: 0)+94
;;; Patch version number. Bug fixes increment this.+95
(patch default: 0)+96
;;; Pre-release identifiers as a list of strings or integers, or `#f` for stable releases.+97
;;; Example: `'("alpha" 1)` for version `1.0.0-alpha.1`+98
(prerelease default: #f)+99
;;; Build metadata string, or `#f` if none. Not used in version comparison.+100
;;; Example: `"20240101"` for version `1.0.0+20240101`+101
(build default: #f))+102
+103
;; ============================================================+104
;; Parsing+105
;; ============================================================+106
+107
;;; Parse a version string into a version record.+108
;;;+109
;;; Returns `#f` if the string is not a valid version. Accepts optional+110
;;; leading `v` or `V` prefix.+111
;;;+112
;;; ```scheme+113
;;; (parse-version "1.2.3") ; => <version 1.2.3>+114
;;; (parse-version "v2.0.0-beta.1") ; => <version 2.0.0-beta.1>+115
;;; (parse-version "1.0.0+build.42") ; => <version 1.0.0+build.42>+116
;;; (parse-version "invalid") ; => #f+117
;;; ```+118
(define (parse-version str)+119
(if (not (string? str))+120
#f+121
(let* ((str (string-trim str))+122
;; Strip leading 'v' or 'V' if present+123
(str (if (and (> (string-length str) 0)+124
(or (char=? (string-ref str 0) #\v)+125
(char=? (string-ref str 0) #\V)))+126
(substring str 1 (string-length str))+127
str)))+128
(parse-version-core str))))+129
+130
;;; Alias for parse-version.+131
(define string->version parse-version)+132
+133
;; Internal: Parse the core version string (without leading 'v')+134
(define (parse-version-core str)+135
(let* (;; Split off build metadata first (+...)+136
(build-split (string-split-char str #\+))+137
(version-part (car build-split))+138
(build-meta (if (> (length build-split) 1)+139
(cadr build-split)+140
#f))+141
;; Split off prerelease (-...)+142
(pre-split (string-split-char version-part #\-))+143
(core-part (car pre-split))+144
(prerelease-str (if (> (length pre-split) 1)+145
(string-join (cdr pre-split) "-")+146
#f))+147
;; Parse core version numbers+148
(parts (string-split-char core-part #\.)))+149
(if (< (length parts) 1)+150
#f+151
(let ((major (string->number (car parts)))+152
(minor (if (> (length parts) 1)+153
(string->number (cadr parts))+154
0))+155
(patch (if (> (length parts) 2)+156
(string->number (caddr parts))+157
0)))+158
(if (and major minor patch+159
(>= major 0) (>= minor 0) (>= patch 0))+160
(version+161
major: major+162
minor: minor+163
patch: patch+164
prerelease: (and prerelease-str+165
(parse-prerelease prerelease-str))+166
build: build-meta)+167
#f)))))+168
+169
;; Internal: Parse prerelease string into list of identifiers+170
(define (parse-prerelease str)+171
(if (or (not str) (string=? str ""))+172
#f+173
(map (lambda (id)+174
(let ((num (string->number id)))+175
(if num num id)))+176
(string-split-char str #\.))))+177
+178
;;; Convert a version to its string representation.+179
;;;+180
;;; ```scheme+181
;;; (version->string (parse-version "1.2.3-alpha"))+182
;;; ; => "1.2.3-alpha"+183
;;; ```+184
(define (version->string v)+185
(let ((core (string-append+186
(number->string (version-major v))+187
"."+188
(number->string (version-minor v))+189
"."+190
(number->string (version-patch v)))))+191
(let ((with-pre (if (version-prerelease v)+192
(string-append core "-"+193
(prerelease->string (version-prerelease v)))+194
core)))+195
(if (version-build v)+196
(string-append with-pre "+" (version-build v))+197
with-pre))))+198
+199
;; Internal: Convert prerelease list to string+200
(define (prerelease->string ids)+201
(string-join (map (lambda (id)+202
(if (number? id)+203
(number->string id)+204
id))+205
ids)+206
"."))+207
+208
;; ============================================================+209
;; Comparison+210
;; ============================================================+211
+212
;;; Compare two versions.+213
;;;+214
;;; Returns `-1` if a < b, `0` if a = b, `1` if a > b.+215
;;; Build metadata is ignored per semver spec.+216
;;;+217
;;; ```scheme+218
;;; (version-compare (parse-version "1.0.0")+219
;;; (parse-version "2.0.0")) ; => -1+220
;;; ```+221
(define (version-compare a b)+222
(let ((major-cmp (compare-numbers (version-major a) (version-major b))))+223
(if (not (= major-cmp 0))+224
major-cmp+225
(let ((minor-cmp (compare-numbers (version-minor a) (version-minor b))))+226
(if (not (= minor-cmp 0))+227
minor-cmp+228
(let ((patch-cmp (compare-numbers (version-patch a) (version-patch b))))+229
(if (not (= patch-cmp 0))+230
patch-cmp+231
(compare-prerelease (version-prerelease a)+232
(version-prerelease b)))))))))+233
+234
;; Internal: Compare two numbers+235
(define (compare-numbers a b)+236
(cond+237
((< a b) -1)+238
((> a b) 1)+239
(else 0)))+240
+241
;; Internal: Compare prerelease identifiers+242
(define (compare-prerelease a b)+243
(cond+244
((and (not a) (not b)) 0)+245
((not a) 1) ; no prerelease > prerelease+246
((not b) -1)+247
(else (compare-prerelease-ids a b))))+248
+249
;; Internal: Compare prerelease identifier lists+250
(define (compare-prerelease-ids a b)+251
(cond+252
((and (null? a) (null? b)) 0)+253
((null? a) -1)+254
((null? b) 1)+255
(else+256
(let ((cmp (compare-prerelease-id (car a) (car b))))+257
(if (not (= cmp 0))+258
cmp+259
(compare-prerelease-ids (cdr a) (cdr b)))))))+260
+261
;; Internal: Compare two prerelease identifiers+262
(define (compare-prerelease-id a b)+263
(cond+264
((and (number? a) (number? b))+265
(compare-numbers a b))+266
((and (string? a) (string? b))+267
(cond+268
((string<? a b) -1)+269
((string>? a b) 1)+270
(else 0)))+271
((number? a) -1) ; number < string+272
(else 1)))+273
+274
;;; Test if two versions are equal.+275
;;;+276
;;; ```scheme+277
;;; (version=? (parse-version "1.0.0")+278
;;; (parse-version "1.0.0")) ; => #t+279
;;; ```+280
(define (version=? a b)+281
(= (version-compare a b) 0))+282
+283
;;; Test if version a is less than version b.+284
(define (version<? a b)+285
(< (version-compare a b) 0))+286
+287
;;; Test if version a is greater than version b.+288
(define (version>? a b)+289
(> (version-compare a b) 0))+290
+291
;;; Test if version a is less than or equal to version b.+292
(define (version<=? a b)+293
(<= (version-compare a b) 0))+294
+295
;;; Test if version a is greater than or equal to version b.+296
(define (version>=? a b)+297
(>= (version-compare a b) 0))+298
+299
;; ============================================================+300
;; Constraints+301
;; ============================================================+302
+303
;;; Parse a version constraint string.+304
;;;+305
;;; Returns a constraint record or `#f` if invalid.+306
;;;+307
;;; ```scheme+308
;;; (parse-constraint ">= 1.0.0") ; => (>= <version>)+309
;;; (parse-constraint "^2.0.0") ; => (caret <version>)+310
;;; ```+311
(define (parse-constraint str)+312
(if (not (string? str))+313
#f+314
(let ((str (string-trim str)))+315
(cond+316
((string=? str "*")+317
(list 'any))+318
((string-prefix? "^" str)+319
(let ((v (parse-version (substring str 1 (string-length str)))))+320
(and v (list 'caret v))))+321
((string-prefix? "~" str)+322
(let ((v (parse-version (substring str 1 (string-length str)))))+323
(and v (list 'tilde v))))+324
((string-prefix? ">=" str)+325
(let ((v (parse-version (string-trim (substring str 2 (string-length str))))))+326
(and v (list '>= v))))+327
((string-prefix? ">" str)+328
(let ((v (parse-version (string-trim (substring str 1 (string-length str))))))+329
(and v (list '> v))))+330
((string-prefix? "<=" str)+331
(let ((v (parse-version (string-trim (substring str 2 (string-length str))))))+332
(and v (list '<= v))))+333
((string-prefix? "<" str)+334
(let ((v (parse-version (string-trim (substring str 1 (string-length str))))))+335
(and v (list '< v))))+336
((string-prefix? "=" str)+337
(let ((v (parse-version (string-trim (substring str 1 (string-length str))))))+338
(and v (list '= v))))+339
(else+340
(let ((v (parse-version str)))+341
(and v (list '= v))))))))+342
+343
;;; Check if a version satisfies a constraint string.+344
;;;+345
;;; ```scheme+346
;;; (version-satisfies? (parse-version "1.5.0") ">= 1.0.0") ; => #t+347
;;; (version-satisfies? (parse-version "2.0.0") "^1.0.0") ; => #f+348
;;; ```+349
(define (version-satisfies? v constraint-str)+350
(let ((constraint (parse-constraint constraint-str)))+351
(and constraint (constraint-satisfies? v constraint))))+352
+353
;;; Check if a version satisfies a parsed constraint.+354
(define (constraint-satisfies? v constraint)+355
(case (car constraint)+356
((any) #t)+357
((=) (version=? v (cadr constraint)))+358
((>) (version>? v (cadr constraint)))+359
((>=) (version>=? v (cadr constraint)))+360
((<) (version<? v (cadr constraint)))+361
((<=) (version<=? v (cadr constraint)))+362
((caret) (caret-satisfies? v (cadr constraint)))+363
((tilde) (tilde-satisfies? v (cadr constraint)))+364
(else #f)))+365
+366
;; Internal: Caret constraint (^X.Y.Z)+367
(define (caret-satisfies? v base)+368
(and (version>=? v base)+369
(cond+370
((> (version-major base) 0)+371
(= (version-major v) (version-major base)))+372
((> (version-minor base) 0)+373
(and (= (version-major v) 0)+374
(= (version-minor v) (version-minor base))))+375
(else+376
(and (= (version-major v) 0)+377
(= (version-minor v) 0)+378
(= (version-patch v) (version-patch base)))))))+379
+380
;; Internal: Tilde constraint (~X.Y.Z)+381
(define (tilde-satisfies? v base)+382
(and (version>=? v base)+383
(= (version-major v) (version-major base))+384
(= (version-minor v) (version-minor base))))+385
+386
;; ============================================================+387
;; Utilities+388
;; ============================================================+389
+390
;;; Check if version a is compatible with version b.+391
;;;+392
;;; Two versions are compatible if they have the same major version+393
;;; and a >= b.+394
;;;+395
;;; ```scheme+396
;;; (version-compatible? (parse-version "1.5.0")+397
;;; (parse-version "1.2.0")) ; => #t+398
;;; ```+399
(define (version-compatible? a b)+400
(and (= (version-major a) (version-major b))+401
(version>=? a b)))+402
+403
;;; Bump the major version (resets minor and patch to 0).+404
;;;+405
;;; ```scheme+406
;;; (version->string (version-bump-major (parse-version "1.2.3")))+407
;;; ; => "2.0.0"+408
;;; ```+409
(define (version-bump-major v)+410
(version+411
major: (+ (version-major v) 1)+412
minor: 0+413
patch: 0))+414
+415
;;; Bump the minor version (resets patch to 0).+416
;;;+417
;;; ```scheme+418
;;; (version->string (version-bump-minor (parse-version "1.2.3")))+419
;;; ; => "1.3.0"+420
;;; ```+421
(define (version-bump-minor v)+422
(version+423
major: (version-major v)+424
minor: (+ (version-minor v) 1)+425
patch: 0))+426
+427
;;; Bump the patch version.+428
;;;+429
;;; ```scheme+430
;;; (version->string (version-bump-patch (parse-version "1.2.3")))+431
;;; ; => "1.2.4"+432
;;; ```+433
(define (version-bump-patch v)+434
(version+435
major: (version-major v)+436
minor: (version-minor v)+437
patch: (+ (version-patch v) 1)))+438
+439
;; ============================================================+440
;; Internal Utilities+441
;; ============================================================+442
+443
(define (string-prefix? prefix str)+444
(string-starts-with? str prefix))+445
+446
(define (string-split-char str ch)+447
(let loop ((chars (string->list str))+448
(current '())+449
(result '()))+450
(cond+451
((null? chars)+452
(reverse (cons (list->string (reverse current)) result)))+453
((char=? (car chars) ch)+454
(loop (cdr chars)+455
'()+456
(cons (list->string (reverse current)) result)))+457
(else+458
(loop (cdr chars)+459
(cons (car chars) current)+460
result)))))+461
+462
(define (string-join strs sep)+463
(if (null? strs)+464
""+465
(let loop ((rest (cdr strs))+466
(result (car strs)))+467
(if (null? rest)+468
result+469
(loop (cdr rest)+470
(string-append result sep (car rest)))))))))