Skip to content

Commit aad37b7

Browse files
gh-16: Update SLICE operator.
1 parent cd16579 commit aad37b7

File tree

5 files changed

+124
-63
lines changed

5 files changed

+124
-63
lines changed

docs/SPECIFICATION.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -255,7 +255,7 @@
255255
256256
Loops are provided via `WHILE`, `FOR`, and `PARFOR`. A `WHILE` loop has the form `WHILE(condition){ block }`. On each iteration, the condition is evaluated; if it is non-zero, the block executes and control returns to the start of the loop to evaluate the condition again. If the condition evaluates to zero, the loop terminates and execution continues with the next statement.
257257
258-
The `FOR` construct has the form `FOR(counter, target){ block }`, where `counter` is an identifier and `target` is an expression. At loop entry, `target` is evaluated once to a value `T` (must be `INT`), and `counter` is initialized to the `INT` value `0`, creating it as an `INT` if necessary. The loop then repeats: if the current value of `counter` is greater than or equal to `T`, the loop exits; otherwise, the block executes and `counter` is incremented by `1` before the next iteration.
258+
The `FOR` construct has the form `FOR(counter, target){ block }`, where `counter` is an identifier and `target` is an expression. At loop entry, `target` is evaluated once to a value `T` (must be `INT`), and `counter` is initialized to the `INT` value `1`, creating it as an `INT` if necessary. The loop then repeats: if the current value of `counter` is greater than `T`, the loop exits; otherwise, the block executes and `counter` is incremented by `1` before the next iteration.
259259
260260
Note on scoping: the loop `counter` is loop-local and does not persist in the enclosing environment after the `FOR` completes (any prior binding with the same name is restored). Any other identifiers that are declared (first assigned with a `TYPE:` declaration) inside the `FOR` body are bound in the enclosing environment (that is, they persist after the loop finishes).
261261
@@ -627,7 +627,7 @@
627627

628628
- `SHR(INT: a, INT: b):INT` ; shift right
629629

630-
- `SLICE(INT|STR: a, INT: hi, INT: lo):INT|STR` ; `INT` target -> bit-slice [hi:lo]; `STR` target -> inclusive char-slice counting from the right (index 0 = last char). `TNS` are not accepted.
630+
- `SLICE(INT|STR: a, INT: start, INT: end):INT|STR` ; `INT` target -> bit-slice [start:end]; `STR` target -> inclusive char-slice counting from the left (index 1 = first char).
631631

632632
- `AND(ANY: a1, ..., ANY: aN):INT` ; Boolean AND
633633

lib/image/init.pre

Lines changed: 30 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,16 @@
22
! Backed by ext/image.py (stdlib-only, prefers Windows GDI+).
33
!
44
! API (operators provided by the model/extension):
5-
! LOAD_PNG(STR: path):TNS[width,height,[r,g,b,a]]
6-
! LOAD_JPEG(STR: path):TNS[width,height,[r,g,b,a]]
7-
! LOAD_BMP(STR: path):TNS[width,height,[r,g,b,a]]
8-
! LOAD(STR: path):TNS[height,width,[r,g,b,a]] ! auto-detects format by extension
5+
! LOAD_PNG(STR: path):TNS[width, height, [r, g, b, a]]
6+
! LOAD_JPEG(STR: path):TNS[width, height, [r, g, b, a]]
7+
! LOAD_BMP(STR: path):TNS[width, height, [r, g, b, a]]
8+
! LOAD(STR: path):TNS[height, width, [r, g, b, a]] ! auto-detects format by extension
99
! SAVE_PNG(TNS: img, STR: path, INT: compression_level)
1010
! SAVE_JPEG(TNS: img, STR: path, INT: compression_level)
1111
! SAVE_BMP(TNS: img, STR: path)
12-
! POLYGON(TNS:img, TNS:points[[x,y]...], TNS:color[r,g,b,a], INT:fill=1, INT:thickness=1):TNS
13-
! RECT(TNS: img, INT: x, INT: y, INT: width, INT: height, TNS: color[r,g,b,a], INT: fill = 1, INT: thickness = 1):TNS ! wraps POLYGON
14-
! ELLIPSE(TNS: img, TNS: center, INT: rx, INT: ry, TNS: color[r,g,b,a], INT: fill = 1, INT: thickness = 1):TNS
12+
! POLYGON(TNS: img, TNS: points[[x, y]...], TNS: color[r, g, b, a], INT: fill=1, INT: thickness=1):TNS
13+
! RECT(TNS: img, INT: x, INT: y, INT: width, INT: height, TNS: color[r, g, b, a], INT: fill = 1, INT: thickness = 1):TNS ! wraps POLYGON
14+
! ELLIPSE(TNS: img, TNS: center, INT: rx, INT: ry, TNS: color[r, g, b, a], INT: fill = 1, INT: thickness = 1):TNS
1515
! SQUARE(TNS: img, INT: x, INT: y, INT: size, TNS: color, INT: fill, INT: thickness):TNS
1616
! CIRCLE(TNS: img, TNS: center, INT: radius, TNS: color, INT: fill, INT: thickness):TNS
1717
! WIDTH(TNS: img):INT
@@ -26,27 +26,27 @@
2626
! PIXEL_G(TNS: img, INT: x, INT: y):INT
2727
! PIXEL_B(TNS: img, INT: x, INT: y):INT
2828
! PIXEL_A(TNS: img, INT: x, INT: y):INT
29-
! THRESHHOLD_A(TNS:img, INT:a, TNS:color=[0,0,0,0]):TNS
30-
! THRESHHOLD_R(TNS:img, INT:r, TNS:color=[0,0,0,0]):TNS
31-
! THRESHHOLD_G(TNS:img, INT:g, TNS:color=[0,0,0,0]):TNS
32-
! THRESHHOLD_B(TNS:img, INT:b, TNS:color=[0,0,0,0]):TNS
29+
! THRESHHOLD_A(TNS: img, INT: a, TNS: color=[0, 0, 0, 0]):TNS
30+
! THRESHHOLD_R(TNS: img, INT: r, TNS: color=[0, 0, 0, 0]):TNS
31+
! THRESHHOLD_G(TNS: img, INT: g, TNS: color=[0, 0, 0, 0]):TNS
32+
! THRESHHOLD_B(TNS: img, INT: b, TNS: color=[0, 0, 0, 0]):TNS
3333
! FLIP_V(TNS: img):TNS
3434
! FLIP_H(TNS: img):TNS
3535
! INVERT(TNS: img):TNS
3636
! SCALE(TNS: src, FLT: scale_x, FLT: scale_y, INT: antialiasing = 1):TNS
3737
! RESIZE(TNS: img, INT: new_width, INT: new_height, INT: antialiasing = 1):TNS
38-
! CROP(TNS:img, TNS:corners[[tl_x,tl_y],[tr_x,tr_y],[bl_x,bl_y],[br_x,br_y]]):TNS
38+
! CROP(TNS: img, TNS: corners[[tl_x, tl_y], [tr_x, tr_y], [bl_x, bl_y], [br_x, br_y]]):TNS
3939
! ROTATE(TNS: img, FLT: degrees):TNS
4040
! BLIT(TNS: src, TNS: dest, INT: x, INT: y, INT: mixalpha = 1):TNS
41-
! GRAYSCALE(TNS: img):TNS ! three-channel grayscale (rgb=luminance, alpha preserved)
41+
! GRAYSCALE(TNS: img):TNS ! three-channel grayscale (rgb = luminance, alpha preserved)
4242
! REPLACE_COLOR(TNS: img, TNS: target_color, TNS: replacement_color):TNS
43-
! BLUR(TNS: img, INT: radius):TNS ! gaussian blur with integer radius
44-
! EDGE(TNS: img):TNS ! difference-of-gaussians edge detector
45-
! CELLSHADE(TNS: img, TNS: colors):TNS ! map pixels to nearest palette color
46-
! SHOW(TNS: img):INT ! saves to temp file and opens with default viewer (Windows only)
43+
! BLUR(TNS: img, INT: radius):TNS ! gaussian blur with integer radius
44+
! EDGE(TNS: img):TNS ! difference-of-gaussians edge detector
45+
! CELLSHADE(TNS: img, TNS: colors):TNS ! map pixels to nearest palette color
46+
! SHOW(TNS: img):INT ! saves to temp file and opens with default viewer (Windows only)
4747
!
4848
! The returned tensor layout is [column][row][channel] (width, height, channel)
49-
! in user code. Channels are ordered r,g,b,a and values are 0..255.
49+
! in user code. Channels are ordered r, g, b, a and values are 0..255.
5050

5151
IMPORT(path)
5252

@@ -119,6 +119,10 @@ FUNC FLIP_H(TNS: img):TNS{
119119
RETURN(TFLIP(img, 10))
120120
}
121121

122+
FUNC INVERT(TNS: img):TNS{
123+
RETURN(MSUB(TNS(SHAPE(img), 111111111), img))
124+
}
125+
122126
FUNC RECT(TNS: img, INT: x, INT: y, INT: width, INT: height, TNS: color, INT: fill, INT: thickness):TNS{
123127
TNS: pts = [ ^
124128
[x, y], ^
@@ -150,6 +154,13 @@ FUNC CIRCLE(TNS: img, TNS: center, INT: radius, TNS: color, INT: fill, INT: thic
150154
RETURN(image.ELLIPSE(img, center, radius, radius, color, fill, thickness))
151155
}
152156

157+
FUNC CROP(TNS: img, TNS: corners):TNS{
158+
IF(NOT(EQ(SHAPE(corners), [100, 10]))){
159+
THROW("CROP corners must be a 100x10 tensor: [[tl_x, tl_y], [tr_x, tr_y], [bl_x, bl_y], [br_x, br_y]]")
160+
}
161+
RETURN(img[MIN(corners[*, 1])-MAX(corners[*, 1]), MIN(corners[*, 10])-MAX(corners[*, 10]), *])
162+
}
163+
153164
FUNC SHOW(TNS: img):INT{
154165
! Save the image to the provided path and open it with the system default
155166
! viewer on Windows. `img_path` is treated as the target file path.
@@ -160,4 +171,4 @@ FUNC SHOW(TNS: img):INT{
160171
win32.WIN_CALL("shell32", "ShellExecuteW", "PSSSSI", "P", 0, "open", img_path, "", "", 1)
161172
DEL(img_path)
162173
RETURN(0)
163-
}
174+
}

lib/numbers.pre

Lines changed: 17 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -83,9 +83,15 @@ FUNC VALUE(STR: num, INT: base):INT{
8383
! we don't rely on SLICE index conventions.
8484
STR: s = num
8585
INT: neg = 0
86+
STR: digit = ""
8687
FOR(i, SLEN(s)){
87-
INT: idx = SUB(SLEN(s), i)
88-
STR: digit = SLICE(s, idx, idx)
88+
INT: pos = i
89+
INT: exp = SUB(SLEN(s), pos)
90+
digit = SLICE(s, pos, pos)
91+
! If SLICE returned an empty string (out-of-range), skip safely.
92+
IF(EQ(digit, "")){
93+
CONTINUE()
94+
}
8995
! If the leftmost character is '-' treat as sign, otherwise error
9096
IF(AND(EQ(digit, "-"), EQ(i, 1))){
9197
neg = 1
@@ -94,7 +100,7 @@ FUNC VALUE(STR: num, INT: base):INT{
94100
IF(NOT(KEYIN(digit, digits))){
95101
THROW(JOIN("Digit \"", digit, "\" not valid for base ", STR(base)))
96102
}
97-
ADD(@value, MUL(digits<digit>, POW(base, idx)))
103+
ADD(@value, MUL(digits<digit>, POW(base, exp)))
98104
! compute internal binary value as digit*base^(position from right)
99105
}
100106
IF(GT(SLEN(s), 0)){
@@ -112,11 +118,9 @@ FUNC CONVERT(STR: num, INT: from, INT: to):STR{
112118
! into integer and fractional parts and convert each appropriately.
113119
STR: s = num
114120
INT: dotpos = 0
115-
! Find '.' by iterating characters left-to-right using the
116-
! same right-indexing convention as VALUE (idx = SLEN(s) - i).
121+
! Find '.' by iterating characters left-to-right.
117122
FOR(i, SLEN(s)){
118-
INT: idx = SUB(SLEN(s), i)
119-
STR: ch = SLICE(s, idx, idx)
123+
STR: ch = SLICE(s, i, i)
120124
IF(EQ(ch, ".")){
121125
dotpos = i
122126
BREAK(1)
@@ -127,16 +131,16 @@ FUNC CONVERT(STR: num, INT: from, INT: to):STR{
127131
STR: fracpart = ""
128132
IF(GT(dotpos, 0)){
129133
! intpart is characters left of the dot. When dot is first
130-
! character, use "0". Otherwise slice from left [0..dotpos-2].
134+
! character, use "0". Otherwise slice from left [1..dotpos-1].
131135
IF(EQ(dotpos, 1)){
132136
intpart = "0"
133137
} ELSE {
134-
STR: intpart = SLICE(s, SUB(SLEN(s), 1), SUB(ADD(SLEN(s), 1), dotpos))
138+
STR: intpart = SLICE(s, 1, SUB(dotpos, 1))
135139
}
136140
! fracpart is characters right of the dot; slice from left
137-
! [dotpos..SLEN(s)-1], which maps to right-indices [SLEN(s)-dotpos-1 .. 0].
141+
! [dotpos+1..SLEN(s)].
138142
IF(LT(dotpos, SLEN(s))){
139-
fracpart = SLICE(s, SUB(SLEN(s), ADD(dotpos, 1)), 0)
143+
fracpart = SLICE(s, ADD(dotpos, 1), SLEN(s))
140144
} ELSE {
141145
fracpart = ""
142146
}
@@ -185,8 +189,8 @@ FUNC CONVERT(STR: num, INT: from, INT: to):STR{
185189
INT: denom = POW(from, SLEN(fracpart))
186190
STR: frac_res = ""
187191
INT: iter = 0
188-
! Limit iterations to avoid infinite repeats; 64 digits is arbitrary
189-
WHILE(AND(GT(frac_num, 0), LT(iter, 1000000))){
192+
! Limit iterations to avoid infinite repeats; 1028 digits is arbitrary
193+
WHILE(AND(GT(frac_num, 0), LT(iter, 10000000000))){
190194
ADD(@iter, 1)
191195
MUL(@frac_num, to)
192196
INT: d = DIV(frac_num, denom)

src/builtins.c

Lines changed: 73 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1028,7 +1028,7 @@ static void ser_expr(JsonBuf* jb, SerCtx* ctx, Interpreter* interp, Expr* expr)
10281028
ser_loc(jb, expr->line, expr->column);
10291029
json_obj_field(jb, &first, "lo");
10301030
ser_expr(jb, ctx, interp, expr->as.range.start);
1031-
json_obj_field(jb, &first, "hi");
1031+
json_obj_field(jb, &first, "start");
10321032
ser_expr(jb, ctx, interp, expr->as.range.end);
10331033
jb_append_char(jb, '}');
10341034
return;
@@ -1815,8 +1815,8 @@ static Expr* deser_expr(JsonValue* obj, UnserCtx* ctx, Interpreter* interp, cons
18151815
}
18161816
if (strcmp(name, "Range") == 0) {
18171817
Expr* lo = deser_expr(json_obj_get(obj, "lo"), ctx, interp, err);
1818-
Expr* hi = deser_expr(json_obj_get(obj, "hi"), ctx, interp, err);
1819-
return expr_range(lo, hi, line, col);
1818+
Expr* start = deser_expr(json_obj_get(obj, "start"), ctx, interp, err);
1819+
return expr_range(lo, start, line, col);
18201820
}
18211821
if (strcmp(name, "Star") == 0) {
18221822
return expr_wildcard(line, col);
@@ -4345,36 +4345,84 @@ static Value builtin_import_path(Interpreter* interp, Value* args, int argc, Exp
43454345
}
43464346
static Value builtin_slice(Interpreter* interp, Value* args, int argc, Expr** arg_nodes, Env* env, int line, int col) {
43474347
(void)arg_nodes; (void)env;
4348-
// SLICE can operate on STR or TNS for now. Syntax: SLICE(target, start, end)
4348+
// SLICE per spec: SLICE(INT|STR: a, INT: start, INT: end)
4349+
// INT -> bit-slice [start:end] (1-based, negatives from end)
4350+
// STR -> inclusive char-slice counting from the left (index 1 = first char)
4351+
if (args[0].type == VAL_INT) {
4352+
EXPECT_INT(args[1], "SLICE", interp, line, col);
4353+
EXPECT_INT(args[2], "SLICE", interp, line, col);
4354+
4355+
int64_t v = args[0].as.i;
4356+
uint64_t u = (v < 0) ? (uint64_t)(-v) : (uint64_t)v;
4357+
4358+
// compute bit length (ILEN semantics)
4359+
int64_t bitlen = 0;
4360+
if (u == 0) bitlen = 1;
4361+
else {
4362+
uint64_t tmp = u;
4363+
while (tmp > 0) { bitlen++; tmp >>= 1; }
4364+
}
4365+
4366+
int64_t start = args[1].as.i;
4367+
int64_t end = args[2].as.i;
4368+
if (start < 0) start = bitlen + start + 1;
4369+
if (end < 0) end = bitlen + end + 1;
4370+
4371+
if (start < 1) start = 1;
4372+
if (end < 1) end = 1;
4373+
if (start > bitlen) start = bitlen;
4374+
if (end > bitlen) end = bitlen;
4375+
4376+
// ensure start <= end for a non-empty inclusive slice
4377+
if (start > end) return value_int(0);
4378+
4379+
// convert positions (1-based from left/MSB) to bit indices (0-based from LSB)
4380+
int64_t hi_bit = bitlen - start; // index of high bit (from LSB)
4381+
int64_t lo_bit = bitlen - end; // index of low bit (from LSB)
4382+
int64_t nbits = hi_bit - lo_bit + 1;
4383+
4384+
uint64_t result = 0;
4385+
if (nbits > 0) {
4386+
// shift right by lo_bit then mask nbits
4387+
result = (u >> lo_bit) & ((nbits >= 64) ? UINT64_MAX : ((1ULL << nbits) - 1ULL));
4388+
}
4389+
4390+
return value_int((int64_t)result);
4391+
}
4392+
43494393
if (args[0].type == VAL_STR) {
43504394
EXPECT_INT(args[1], "SLICE", interp, line, col);
43514395
EXPECT_INT(args[2], "SLICE", interp, line, col);
43524396
const char* s = args[0].as.s;
43534397
size_t len = strlen(s);
4354-
int64_t start = args[1].as.i;
4355-
int64_t end = args[2].as.i;
4356-
if (start < 0) start = (int64_t)len + start + 1;
4357-
if (end < 0) end = (int64_t)len + end + 1;
4358-
start--;
4359-
if (start < 0) start = 0;
4360-
if (end > (int64_t)len) end = (int64_t)len;
4361-
if (start >= end) return value_str("");
4362-
size_t result_len = (size_t)(end - start);
4398+
/* Treat string slice arguments as start,end (first -> start, second -> end).
4399+
This matches test usage where callers pass start then end positions. */
4400+
int64_t start = args[1].as.i;
4401+
int64_t end = args[2].as.i;
4402+
if (start < 0) start = (int64_t)len + start + 1;
4403+
if (end < 0) end = (int64_t)len + end + 1;
4404+
4405+
if (start < 1) start = 1;
4406+
if (end < 1) end = 1;
4407+
if (start > (int64_t)len) start = (int64_t)len;
4408+
if (end > (int64_t)len) end = (int64_t)len;
4409+
4410+
/* inclusive indices: start..end */
4411+
int64_t low_idx = start - 1;
4412+
int64_t high_idx = end - 1;
4413+
if (low_idx > high_idx) return value_str("");
4414+
4415+
size_t result_len = (size_t)(high_idx - low_idx + 1);
43634416
char* result = malloc(result_len + 1);
4364-
memcpy(result, s + start, result_len);
4417+
if (!result) { RUNTIME_ERROR(interp, "Out of memory", line, col); }
4418+
memcpy(result, s + low_idx, result_len);
43654419
result[result_len] = '\0';
43664420
Value v = value_str(result);
43674421
free(result);
43684422
return v;
4369-
} else if (args[0].type == VAL_TNS) {
4370-
// slice along first axis using 1-based inclusive indices
4371-
EXPECT_INT(args[1], "SLICE", interp, line, col);
4372-
EXPECT_INT(args[2], "SLICE", interp, line, col);
4373-
int64_t starts[1] = { args[1].as.i };
4374-
int64_t ends[1] = { args[2].as.i };
4375-
return value_tns_slice(args[0], starts, ends, 1);
43764423
}
4377-
RUNTIME_ERROR(interp, "SLICE expects STR or TNS", line, col);
4424+
4425+
RUNTIME_ERROR(interp, "SLICE expects INT or STR", line, col);
43784426
}
43794427

43804428
static Value builtin_replace(Interpreter* interp, Value* args, int argc, Expr** arg_nodes, Env* env, int line, int col) {
@@ -4719,10 +4767,10 @@ static Value builtin_writefile(Interpreter* interp, Value* args, int argc, Expr*
47194767
}
47204768
for (size_t i = 0; i < blen; i += 2) {
47214769
char a = blob[i]; char b = blob[i+1];
4722-
int hi = (a >= '0' && a <= '9') ? a - '0' : (a >= 'a' && a <= 'f') ? a - 'a' + 10 : (a >= 'A' && a <= 'F') ? a - 'A' + 10 : -1;
4770+
int start = (a >= '0' && a <= '9') ? a - '0' : (a >= 'a' && a <= 'f') ? a - 'a' + 10 : (a >= 'A' && a <= 'F') ? a - 'A' + 10 : -1;
47234771
int lo = (b >= '0' && b <= '9') ? b - '0' : (b >= 'a' && b <= 'f') ? b - 'a' + 10 : (b >= 'A' && b <= 'F') ? b - 'A' + 10 : -1;
4724-
if (hi < 0 || lo < 0) { fclose(f); RUNTIME_ERROR(interp, "WRITEFILE(hex) expects valid hex digits", line, col); }
4725-
unsigned char byte = (unsigned char)((hi << 4) | lo);
4772+
if (start < 0 || lo < 0) { fclose(f); RUNTIME_ERROR(interp, "WRITEFILE(hex) expects valid hex digits", line, col); }
4773+
unsigned char byte = (unsigned char)((start << 4) | lo);
47264774
if (fwrite(&byte, 1, 1, f) != 1) { fclose(f); return value_int(0); }
47274775
}
47284776
fclose(f);

test2.pre

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
1-
! Stage 5 Test - Tests INT, FLT, STR, FUNC, TNS, MAP, THR
2-
3-
PRINT("=== Stage 5 Tests ===")
1+
PRINT("=== Testing built-ins... ===\n")
42

53
! --- Literal formats ---
64
PRINT("Testing literals...")
@@ -996,4 +994,4 @@ DEL(caught_su)
996994

997995
PRINT("SER/UNSER: PASS\n")
998996

999-
PRINT("=== All Stage 5 Tests PASSED ===")
997+
PRINT("=== Built-in tests passed. ===")

0 commit comments

Comments
 (0)