From 779c5c082f12b661a6607f7f8df0acfffbf6ece7 Mon Sep 17 00:00:00 2001 From: kf Date: Mon, 20 Apr 2026 16:45:54 +0900 Subject: [PATCH 01/26] feat: add arithmetic operations to set() --- src/command.zig | 72 ++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 65 insertions(+), 7 deletions(-) diff --git a/src/command.zig b/src/command.zig index 66bda462..50ae60ef 100644 --- a/src/command.zig +++ b/src/command.zig @@ -407,14 +407,36 @@ pub fn init() !void { try registry.put(.{ .executable = .{ .name = "SET", .parameters = &[_]Command.Executable.Parameter{ - .{ .name = "variable", .resolve = false }, - .{ .name = "value" }, + .{ .name = "name", .resolve = false }, + .{ .name = "value1", .resolve = true }, + .{ .name = "operator", .resolve = false, .optional = true }, + .{ .name = "value2", .resolve = true, .optional = true }, }, .short_description = "Set a variable equal to a value.", .long_description = \\Create or update a variable name that resolves to the provided value - \\in all future commands. Variable names are case sensitive and shall - \\not begin with digit. + \\in all future commands. If no operator is provided, the variable is + \\set directly to value1. Optional if an operator is provided, the + \\result of the operation is assigned. Variable names are case sensitive + \\and shall not begin with digit. + \\ + \\Example: Set variable 'var' to the value 5 and variable 'var2' to + \\value 'line1'. + \\SET var 5 + \\SET var2 line1 + \\ + \\Example: Set variable 'var' to value of 'var2'. + \\SET var var2 + \\ + \\Example: Set variable 'var' to value of variable 'var2' plus 5. + \\SET var var2 + 5 + \\ + \\Operators: + \\* Addition + + \\* Subtraction - + \\* Multiplication * + \\* Division / + \\* Modulo % , .execute = &set, } }); @@ -779,12 +801,48 @@ fn version(_: [][]const u8) !void { fn set(params: [][]const u8) !void { if (std.ascii.isDigit(params[0][0])) return error.InvalidParameter; - try variables.put(params[0], params[1]); + const name: []const u8 = params[0]; + const value1: []const u8 = params[1]; + const op: []const u8 = params[2]; + const value2: []const u8 = params[3]; + + // Simple assign + if (std.mem.eql(u8, op, "")) { + try variables.put(name, value1); + std.log.info("Variable '{s}': {s}", .{ name, value1 }); + return; + } + + // Compute and assign + if (value1.len == 0 or value2.len == 0 or op.len != 1) + return error.InvalidParameter; + + const a: u16 = try std.fmt.parseInt(u8, value1, 10); + const b: u16 = try std.fmt.parseInt(u8, value2, 10); + const result: u16 = switch (op[0]) { + '+' => try std.math.add(u16, a, b), + '-' => try std.math.sub(u16, a, b), + '*' => try std.math.mul(u16, a, b), + '/' => try std.math.divTrunc(u16, a, b), + '%' => try std.math.mod(u16, a, b), + else => { + std.log.err("Invalid operator: {s}", .{op}); + return error.InvalidParameter; + }, + }; + + const max_len = + comptime std.fmt.count("{}", .{std.math.maxInt(@TypeOf(result))}); + var buf: [max_len]u8 = undefined; + const out = try std.fmt.bufPrint(&buf, "{d}", .{result}); + + try variables.put(name, out); + std.log.info("Variable '{s}': {s}", .{ name, out }); } fn get(params: [][]const u8) !void { if (variables.get(params[0])) |value| { - std.log.info("Variable \"{s}\": {s}\n", .{ + std.log.info("Variable '{s}': {s}\n", .{ params[0], value, }); @@ -795,7 +853,7 @@ fn remove(params: [][]const u8) !void { if (std.ascii.isDigit(params[0][0])) { return error.InvalidParameter; } else if (variables.get(params[0])) |value| { - std.log.info("Remove variable \"{s}\": {s}\n", .{ + std.log.info("Remove variable '{s}': {s}\n", .{ params[0], value, }); From db4af86c2e1296331eb89bc9143835b1f8978383 Mon Sep 17 00:00:00 2001 From: kf Date: Fri, 24 Apr 2026 17:28:15 +0900 Subject: [PATCH 02/26] feature: add 'rest' functionality to parseAndRun() --- src/command.zig | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/src/command.zig b/src/command.zig index 50ae60ef..a4a559f4 100644 --- a/src/command.zig +++ b/src/command.zig @@ -220,6 +220,8 @@ pub const Command = union(enum) { optional: bool = false, quotable: bool = true, resolve: bool = true, + /// The 'rest' parameter can only be used as the last parameter. + rest: bool = false, fn resolveKind() type { // Calculate how many parameters are there that has parsing rule. @@ -707,11 +709,21 @@ fn parseAndRun(input: []const u8) !void { } params[i] = input[start_ind .. start_ind + len]; } else params[i] = token; - } else { - params[i] = token; + } else params[i] = token; + + if (param.rest) { + params[i] = token_iterator.rest(); + break; } } - if (token_iterator.peek() != null) return error.UnexpectedParameter; + + const is_rest: bool = + command.parameters.len > 0 and + command.parameters[command.parameters.len - 1].rest; + + if (!is_rest and token_iterator.peek() != null) + return error.UnexpectedParameter; + try command.execute(params); } From e213bd1f4edb714b4f15ac82b066aae3babcd07a Mon Sep 17 00:00:00 2001 From: kf Date: Mon, 27 Apr 2026 15:43:05 +0900 Subject: [PATCH 03/26] feature: add integer calc() to set() --- src/command.zig | 252 +++++++++++++++++++++++++++++++++++------------- 1 file changed, 183 insertions(+), 69 deletions(-) diff --git a/src/command.zig b/src/command.zig index a4a559f4..f595ac83 100644 --- a/src/command.zig +++ b/src/command.zig @@ -406,42 +406,44 @@ pub fn init() !void { .long_description = "Clear visible screen output.", .execute = &clear, } }); - try registry.put(.{ .executable = .{ - .name = "SET", - .parameters = &[_]Command.Executable.Parameter{ - .{ .name = "name", .resolve = false }, - .{ .name = "value1", .resolve = true }, - .{ .name = "operator", .resolve = false, .optional = true }, - .{ .name = "value2", .resolve = true, .optional = true }, + try registry.put(.{ + .executable = .{ + .name = "SET", + .parameters = &[_]Command.Executable.Parameter{ + .{ .name = "name", .resolve = false }, + .{ .name = "value", .resolve = true, .rest = true }, + // .{ .name = "expression", .resolve = true, .optional = true, .rest = true }, + // .{ .name = "value2", .resolve = true, .optional = true }, + }, + .short_description = "Set a variable equal to a value.", + .long_description = + \\Create or update a variable name that resolves to the provided value + \\in all future commands. If no operator is provided, the variable is + \\set directly to value1. Optional if an operator is provided, the + \\result of the operation is assigned. Variable names are case sensitive + \\and shall not begin with digit. + \\ + \\Example: Set variable 'var' to the value 5 and variable 'var2' to + \\value 'line1'. + \\SET var 5 + \\SET var2 line1 + \\ + \\Example: Set variable 'var' to value of 'var2'. + \\SET var var2 + \\ + \\Example: Set variable 'var' to value of variable 'var2' plus 5. + \\SET var var2 + 5 + \\ + \\Operators: + \\* Addition + + \\* Subtraction - + \\* Multiplication * + \\* Division / + \\* Modulo % + , + .execute = &set, }, - .short_description = "Set a variable equal to a value.", - .long_description = - \\Create or update a variable name that resolves to the provided value - \\in all future commands. If no operator is provided, the variable is - \\set directly to value1. Optional if an operator is provided, the - \\result of the operation is assigned. Variable names are case sensitive - \\and shall not begin with digit. - \\ - \\Example: Set variable 'var' to the value 5 and variable 'var2' to - \\value 'line1'. - \\SET var 5 - \\SET var2 line1 - \\ - \\Example: Set variable 'var' to value of 'var2'. - \\SET var var2 - \\ - \\Example: Set variable 'var' to value of variable 'var2' plus 5. - \\SET var var2 + 5 - \\ - \\Operators: - \\* Addition + - \\* Subtraction - - \\* Multiplication * - \\* Division / - \\* Modulo % - , - .execute = &set, - } }); + }); try registry.put(.{ .executable = .{ .name = "GET", .parameters = &[_]Command.Executable.Parameter{ @@ -660,10 +662,7 @@ fn parseAndRun(input: []const u8) !void { var command: *Command.Executable = undefined; var command_buf: [256]u8 = undefined; if (token_iterator.next()) |token| { - if (registry.getPtr(std.ascii.upperString( - &command_buf, - token, - ))) |c| { + if (registry.getPtr(std.ascii.upperString(&command_buf, token))) |c| { command = c; } else return error.InvalidCommand; } else return; @@ -675,6 +674,7 @@ fn parseAndRun(input: []const u8) !void { defer allocator.free(params); for (command.parameters, 0..) |param, i| { + std.log.debug("cmd parameters: {}, {d}", .{ param, i }); const _token = token_iterator.peek(); defer _ = token_iterator.next(); if (_token == null) { @@ -814,42 +814,156 @@ fn version(_: [][]const u8) !void { fn set(params: [][]const u8) !void { if (std.ascii.isDigit(params[0][0])) return error.InvalidParameter; const name: []const u8 = params[0]; - const value1: []const u8 = params[1]; - const op: []const u8 = params[2]; - const value2: []const u8 = params[3]; - - // Simple assign - if (std.mem.eql(u8, op, "")) { - try variables.put(name, value1); - std.log.info("Variable '{s}': {s}", .{ name, value1 }); + const value: []const u8 = params[1]; + + for (params, 0..) |param, j| { + std.log.debug("param {d}: '{s}'", .{ j, param }); + } + + if (value[0] == '=') { + // Compute and assign + std.log.info("Calc '{s}' = '{s}'", .{ name, value[1..] }); + + var buf: [11]u8 = undefined; + const res = try std.fmt.bufPrint(&buf, "{d}", .{try calc(value[1..])}); + std.log.info("Calc '{s}' = '{s}'", .{ name, res }); + try variables.put(name, res); + return; + } else { + // Simple assign + try variables.put(name, value); + std.log.info("Variable '{s}': {s}", .{ name, value }); return; } +} - // Compute and assign - if (value1.len == 0 or value2.len == 0 or op.len != 1) - return error.InvalidParameter; +const Parser = struct { + input: []const u8, + pos: usize = 0, - const a: u16 = try std.fmt.parseInt(u8, value1, 10); - const b: u16 = try std.fmt.parseInt(u8, value2, 10); - const result: u16 = switch (op[0]) { - '+' => try std.math.add(u16, a, b), - '-' => try std.math.sub(u16, a, b), - '*' => try std.math.mul(u16, a, b), - '/' => try std.math.divTrunc(u16, a, b), - '%' => try std.math.mod(u16, a, b), - else => { - std.log.err("Invalid operator: {s}", .{op}); - return error.InvalidParameter; - }, - }; + fn peek(self: *Parser) ?u8 { + std.log.debug("peek", .{}); + if (self.pos >= self.input.len) return null; + return self.input[self.pos]; + } + + fn skipSpaces(self: *Parser) void { + while (self.pos < self.input.len and + std.ascii.isWhitespace(self.input[self.pos])) : (self.pos += 1) + {} + } + + fn consume(self: *Parser, char: u8) bool { + std.log.debug("consume", .{}); + self.skipSpaces(); + if (self.pos < self.input.len and self.input[self.pos] == char) { + std.log.debug("consume char: {c}", .{char}); + self.pos += 1; + return true; + } + return false; + } + + fn parseExpression(self: *Parser) !i32 { + std.log.debug("parseExpression", .{}); + var lhs = try self.parseTerm(); + + while (true) { + self.skipSpaces(); + const op = self.peek() orelse break; + if (op != '+' and op != '-') break; + + self.pos += 1; + const rhs = try self.parseTerm(); - const max_len = - comptime std.fmt.count("{}", .{std.math.maxInt(@TypeOf(result))}); - var buf: [max_len]u8 = undefined; - const out = try std.fmt.bufPrint(&buf, "{d}", .{result}); + std.log.debug("parseExpression: {d} {c} {d}", .{ lhs, op, rhs }); + + lhs = switch (op) { + '+' => try std.math.add(i32, lhs, rhs), + '-' => try std.math.sub(i32, lhs, rhs), + else => unreachable, + }; + } + + return lhs; + } + + fn parseTerm(self: *Parser) !i32 { + std.log.debug("parseTerm", .{}); + var lhs = try self.parseFactor(); + + while (true) { + self.skipSpaces(); + const op = self.peek() orelse break; + if (op != '*' and op != '/' and op != '%') break; + + self.pos += 1; + const rhs = try self.parseFactor(); + + lhs = switch (op) { + '*' => try std.math.mul(i32, lhs, rhs), + '/' => try std.math.divTrunc(i32, lhs, rhs), + '%' => try std.math.mod(i32, lhs, rhs), + else => unreachable, + }; + } + + return lhs; + } + + fn parseFactor(self: *Parser) !i32 { + std.log.debug("parseFactor", .{}); + self.skipSpaces(); + + if (self.consume('+')) return self.parseFactor(); + + if (self.consume('-')) { + const value = try self.parseFactor(); + return try std.math.sub(i32, 0, value); + } + + return self.parseNumber(); + } + + fn parseNumber(self: *Parser) !i32 { + self.skipSpaces(); + + const start = self.pos; + while (self.pos < self.input.len and std.ascii.isDigit(self.input[self.pos])) : (self.pos += 1) {} + + return try std.fmt.parseInt(i32, self.input[start..self.pos], 10); + } +}; + +pub fn calc(input: []const u8) !i32 { + var parser = Parser{ .input = input }; + + const value = try parser.parseExpression(); + + parser.skipSpaces(); + if (parser.pos != parser.input.len) return error.TrailingCharacters; + + return value; +} + +test "basic precedence" { + try std.testing.expectEqual(14, try calc("2 + 3 * 4")); +} + +test "whitespace and unary minus" { + try std.testing.expectEqual(30, try calc(" 20 + 5 * 2 ")); +} + +test "division and modulo" { + try std.testing.expectEqual(5, try calc("17 % 5 + 6 / 2")); +} + +test "no spaces" { + try std.testing.expectEqual(5, try calc("17%5+6/2")); +} - try variables.put(name, out); - std.log.info("Variable '{s}': {s}", .{ name, out }); +test "division" { + try std.testing.expectEqual(0, try calc("1/3")); } fn get(params: [][]const u8) !void { From c314e71e98c25c6086bf5612f38973a057741b64 Mon Sep 17 00:00:00 2001 From: kf Date: Mon, 27 Apr 2026 16:01:46 +0900 Subject: [PATCH 04/26] feature: calc() calculate as float and round to int --- src/command.zig | 33 ++++++++++++++++++++------------- 1 file changed, 20 insertions(+), 13 deletions(-) diff --git a/src/command.zig b/src/command.zig index f595ac83..46a13acf 100644 --- a/src/command.zig +++ b/src/command.zig @@ -864,7 +864,7 @@ const Parser = struct { return false; } - fn parseExpression(self: *Parser) !i32 { + fn parseExpression(self: *Parser) !f32 { std.log.debug("parseExpression", .{}); var lhs = try self.parseTerm(); @@ -879,8 +879,8 @@ const Parser = struct { std.log.debug("parseExpression: {d} {c} {d}", .{ lhs, op, rhs }); lhs = switch (op) { - '+' => try std.math.add(i32, lhs, rhs), - '-' => try std.math.sub(i32, lhs, rhs), + '+' => lhs + rhs, + '-' => lhs - rhs, else => unreachable, }; } @@ -888,7 +888,7 @@ const Parser = struct { return lhs; } - fn parseTerm(self: *Parser) !i32 { + fn parseTerm(self: *Parser) !f32 { std.log.debug("parseTerm", .{}); var lhs = try self.parseFactor(); @@ -901,9 +901,15 @@ const Parser = struct { const rhs = try self.parseFactor(); lhs = switch (op) { - '*' => try std.math.mul(i32, lhs, rhs), - '/' => try std.math.divTrunc(i32, lhs, rhs), - '%' => try std.math.mod(i32, lhs, rhs), + '*' => lhs * rhs, + '/' => blk: { + if (rhs == 0.0) return error.DivisionByZero; + break :blk lhs / rhs; + }, + '%' => blk: { + if (rhs == 0.0) return error.DivisionByZero; + break :blk @mod(lhs, rhs); + }, else => unreachable, }; } @@ -911,7 +917,7 @@ const Parser = struct { return lhs; } - fn parseFactor(self: *Parser) !i32 { + fn parseFactor(self: *Parser) !f32 { std.log.debug("parseFactor", .{}); self.skipSpaces(); @@ -919,19 +925,19 @@ const Parser = struct { if (self.consume('-')) { const value = try self.parseFactor(); - return try std.math.sub(i32, 0, value); + return value * -1.0; } return self.parseNumber(); } - fn parseNumber(self: *Parser) !i32 { + fn parseNumber(self: *Parser) !f32 { self.skipSpaces(); const start = self.pos; while (self.pos < self.input.len and std.ascii.isDigit(self.input[self.pos])) : (self.pos += 1) {} - return try std.fmt.parseInt(i32, self.input[start..self.pos], 10); + return try std.fmt.parseFloat(f32, self.input[start..self.pos]); } }; @@ -943,7 +949,8 @@ pub fn calc(input: []const u8) !i32 { parser.skipSpaces(); if (parser.pos != parser.input.len) return error.TrailingCharacters; - return value; + std.log.debug("Exact reuslt: {d:.2}", .{value}); + return @as(i32, @intFromFloat(@round(value))); } test "basic precedence" { @@ -963,7 +970,7 @@ test "no spaces" { } test "division" { - try std.testing.expectEqual(0, try calc("1/3")); + try std.testing.expectEqual(1, try calc("1/3 + 1/3")); } fn get(params: [][]const u8) !void { From d9837d183584d5e83567fe340f101f53cc1386fc Mon Sep 17 00:00:00 2001 From: kf Date: Mon, 27 Apr 2026 17:16:22 +0900 Subject: [PATCH 05/26] feature: add '(' and ')' to calc() --- src/command.zig | 80 ++++++++++++++++++++++++++++++------------------- 1 file changed, 49 insertions(+), 31 deletions(-) diff --git a/src/command.zig b/src/command.zig index 46a13acf..4ca5d042 100644 --- a/src/command.zig +++ b/src/command.zig @@ -837,23 +837,32 @@ fn set(params: [][]const u8) !void { } } -const Parser = struct { +const CalcError = error{ + DivisionByZero, + ExpectedClosingParentheses, + TrailingCharacters, + InvalidCharacter, + ExpectedNumber, + UndefinedVariable, + InvalidVariableValue, +}; + +const CalcParser = struct { input: []const u8, pos: usize = 0, - fn peek(self: *Parser) ?u8 { - std.log.debug("peek", .{}); + fn peek(self: *CalcParser) ?u8 { if (self.pos >= self.input.len) return null; return self.input[self.pos]; } - fn skipSpaces(self: *Parser) void { + fn skipSpaces(self: *CalcParser) void { while (self.pos < self.input.len and std.ascii.isWhitespace(self.input[self.pos])) : (self.pos += 1) {} } - fn consume(self: *Parser, char: u8) bool { + fn consume(self: *CalcParser, char: u8) bool { std.log.debug("consume", .{}); self.skipSpaces(); if (self.pos < self.input.len and self.input[self.pos] == char) { @@ -864,8 +873,7 @@ const Parser = struct { return false; } - fn parseExpression(self: *Parser) !f32 { - std.log.debug("parseExpression", .{}); + fn parseExpression(self: *CalcParser) CalcError!f32 { var lhs = try self.parseTerm(); while (true) { @@ -875,9 +883,6 @@ const Parser = struct { self.pos += 1; const rhs = try self.parseTerm(); - - std.log.debug("parseExpression: {d} {c} {d}", .{ lhs, op, rhs }); - lhs = switch (op) { '+' => lhs + rhs, '-' => lhs - rhs, @@ -888,7 +893,7 @@ const Parser = struct { return lhs; } - fn parseTerm(self: *Parser) !f32 { + fn parseTerm(self: *CalcParser) CalcError!f32 { std.log.debug("parseTerm", .{}); var lhs = try self.parseFactor(); @@ -917,32 +922,41 @@ const Parser = struct { return lhs; } - fn parseFactor(self: *Parser) !f32 { + fn parseFactor(self: *CalcParser) CalcError!f32 { std.log.debug("parseFactor", .{}); self.skipSpaces(); if (self.consume('+')) return self.parseFactor(); - if (self.consume('-')) { const value = try self.parseFactor(); - return value * -1.0; + return -value; + } + + if (self.consume('(')) { + const value = try self.parseExpression(); + if (!self.consume(')')) return error.ExpectedClosingParentheses; + return value; } return self.parseNumber(); } - fn parseNumber(self: *Parser) !f32 { + fn parseNumber(self: *CalcParser) CalcError!f32 { self.skipSpaces(); const start = self.pos; while (self.pos < self.input.len and std.ascii.isDigit(self.input[self.pos])) : (self.pos += 1) {} - return try std.fmt.parseFloat(f32, self.input[start..self.pos]); + if (self.pos == start) return error.ExpectedNumber; + + return std.fmt.parseFloat(f32, self.input[start..self.pos]) catch { + return error.InvalidCharacter; + }; } }; -pub fn calc(input: []const u8) !i32 { - var parser = Parser{ .input = input }; +pub fn calc(input: []const u8) CalcError!i32 { + var parser = CalcParser{ .input = input }; const value = try parser.parseExpression(); @@ -953,24 +967,28 @@ pub fn calc(input: []const u8) !i32 { return @as(i32, @intFromFloat(@round(value))); } -test "basic precedence" { +test "calc" { try std.testing.expectEqual(14, try calc("2 + 3 * 4")); -} - -test "whitespace and unary minus" { try std.testing.expectEqual(30, try calc(" 20 + 5 * 2 ")); -} - -test "division and modulo" { try std.testing.expectEqual(5, try calc("17 % 5 + 6 / 2")); -} - -test "no spaces" { try std.testing.expectEqual(5, try calc("17%5+6/2")); -} - -test "division" { try std.testing.expectEqual(1, try calc("1/3 + 1/3")); + try std.testing.expectEqual(72, try calc("(2+2)*2*(3+3*2)")); + + try std.testing.expectError(error.DivisionByZero, calc("2/0")); + try std.testing.expectError(error.DivisionByZero, calc("2%0")); + try std.testing.expectError(error.ExpectedClosingParentheses, calc("(((2+1)*(((1+1))))*(((2-1)))")); + try std.testing.expectError(error.ExpectedClosingParentheses, calc("2 +2*( 2-1")); + try std.testing.expectError(error.ExpectedClosingParentheses, calc("(2 +2*( 2-1) + 2")); + try std.testing.expectError(error.TrailingCharacters, calc("2 + 2 abc")); //Todo: check if variable and multiply + try std.testing.expectError(error.TrailingCharacters, calc("2 + 2 )")); + try std.testing.expectError(error.TrailingCharacters, calc("2 + 2 (3 + 2)")); //Todo: this should be a multiplication + try std.testing.expectError(error.TrailingCharacters, calc("2 + 2(3 + 2)")); //Todo: this should be a multiplication + try std.testing.expectError(error.TrailingCharacters, calc("2 + 2 @")); + try std.testing.expectError(error.ExpectedNumber, calc("2+2+")); + try std.testing.expectError(error.ExpectedNumber, calc("2+a")); //Todo: check if variable and multiply + try std.testing.expectError(error.ExpectedNumber, calc("2+ a")); //Todo: check if variable and multiply + try std.testing.expectError(error.ExpectedNumber, calc("2+ @")); } fn get(params: [][]const u8) !void { From f90abe81873d66f5f3b3d2e7e475611310f87338 Mon Sep 17 00:00:00 2001 From: kf Date: Mon, 27 Apr 2026 18:28:26 +0900 Subject: [PATCH 06/26] feature: add variable support to calc() --- src/command.zig | 56 +++++++++++++++++++++++++++++++------------------ 1 file changed, 36 insertions(+), 20 deletions(-) diff --git a/src/command.zig b/src/command.zig index 4ca5d042..21b33cc5 100644 --- a/src/command.zig +++ b/src/command.zig @@ -411,7 +411,7 @@ pub fn init() !void { .name = "SET", .parameters = &[_]Command.Executable.Parameter{ .{ .name = "name", .resolve = false }, - .{ .name = "value", .resolve = true, .rest = true }, + .{ .name = "value", .resolve = false, .rest = true }, // .{ .name = "expression", .resolve = true, .optional = true, .rest = true }, // .{ .name = "value2", .resolve = true, .optional = true }, }, @@ -824,9 +824,11 @@ fn set(params: [][]const u8) !void { // Compute and assign std.log.info("Calc '{s}' = '{s}'", .{ name, value[1..] }); - var buf: [11]u8 = undefined; + var buf: [ + std.fmt.count("{d}", .{std.math.minInt(@TypeOf(try calc("1")))}) + ]u8 = undefined; const res = try std.fmt.bufPrint(&buf, "{d}", .{try calc(value[1..])}); - std.log.info("Calc '{s}' = '{s}'", .{ name, res }); + std.log.info("Variable '{s}': {s}", .{ name, res }); try variables.put(name, res); return; } else { @@ -858,15 +860,12 @@ const CalcParser = struct { fn skipSpaces(self: *CalcParser) void { while (self.pos < self.input.len and - std.ascii.isWhitespace(self.input[self.pos])) : (self.pos += 1) - {} + std.ascii.isWhitespace(self.input[self.pos])) self.pos += 1; } fn consume(self: *CalcParser, char: u8) bool { - std.log.debug("consume", .{}); self.skipSpaces(); if (self.pos < self.input.len and self.input[self.pos] == char) { - std.log.debug("consume char: {c}", .{char}); self.pos += 1; return true; } @@ -889,12 +888,10 @@ const CalcParser = struct { else => unreachable, }; } - return lhs; } fn parseTerm(self: *CalcParser) CalcError!f32 { - std.log.debug("parseTerm", .{}); var lhs = try self.parseFactor(); while (true) { @@ -904,7 +901,6 @@ const CalcParser = struct { self.pos += 1; const rhs = try self.parseFactor(); - lhs = switch (op) { '*' => lhs * rhs, '/' => blk: { @@ -918,12 +914,10 @@ const CalcParser = struct { else => unreachable, }; } - return lhs; } fn parseFactor(self: *CalcParser) CalcError!f32 { - std.log.debug("parseFactor", .{}); self.skipSpaces(); if (self.consume('+')) return self.parseFactor(); @@ -938,19 +932,45 @@ const CalcParser = struct { return value; } + const c = self.peek() orelse return error.ExpectedNumber; + if (std.ascii.isAlphabetic(c)) return self.parseVariable(); + if (std.ascii.isDigit(c)) return self.parseNumber(); + return self.parseNumber(); } fn parseNumber(self: *CalcParser) CalcError!f32 { self.skipSpaces(); - const start = self.pos; - while (self.pos < self.input.len and std.ascii.isDigit(self.input[self.pos])) : (self.pos += 1) {} + + while (self.pos < self.input.len and + std.ascii.isDigit(self.input[self.pos])) self.pos += 1; if (self.pos == start) return error.ExpectedNumber; - return std.fmt.parseFloat(f32, self.input[start..self.pos]) catch { + return std.fmt.parseFloat(f32, self.input[start..self.pos]) catch + return error.InvalidCharacter; + } + + fn parseVariable(self: *CalcParser) CalcError!f32 { + self.skipSpaces(); + const start = self.pos; + + if (self.pos >= self.input.len) return error.ExpectedNumber; + if (!std.ascii.isAlphabetic(self.input[self.pos])) return error.InvalidCharacter; + + self.pos += 1; + while (self.pos < self.input.len and + std.ascii.isAlphanumeric(self.input[self.pos])) self.pos += 1; + + const name = self.input[start..self.pos]; + const value_string = variables.get(name) orelse + return error.UndefinedVariable; + std.log.info("{s}: {s}", .{ name, value_string }); + + return std.fmt.parseFloat(f32, value_string) catch { + return error.InvalidVariableValue; }; } }; @@ -962,8 +982,6 @@ pub fn calc(input: []const u8) CalcError!i32 { parser.skipSpaces(); if (parser.pos != parser.input.len) return error.TrailingCharacters; - - std.log.debug("Exact reuslt: {d:.2}", .{value}); return @as(i32, @intFromFloat(@round(value))); } @@ -980,14 +998,12 @@ test "calc" { try std.testing.expectError(error.ExpectedClosingParentheses, calc("(((2+1)*(((1+1))))*(((2-1)))")); try std.testing.expectError(error.ExpectedClosingParentheses, calc("2 +2*( 2-1")); try std.testing.expectError(error.ExpectedClosingParentheses, calc("(2 +2*( 2-1) + 2")); - try std.testing.expectError(error.TrailingCharacters, calc("2 + 2 abc")); //Todo: check if variable and multiply + try std.testing.expectError(error.TrailingCharacters, calc("2 + 2 abc")); //Todo: check if variable if yes multiply try std.testing.expectError(error.TrailingCharacters, calc("2 + 2 )")); try std.testing.expectError(error.TrailingCharacters, calc("2 + 2 (3 + 2)")); //Todo: this should be a multiplication try std.testing.expectError(error.TrailingCharacters, calc("2 + 2(3 + 2)")); //Todo: this should be a multiplication try std.testing.expectError(error.TrailingCharacters, calc("2 + 2 @")); try std.testing.expectError(error.ExpectedNumber, calc("2+2+")); - try std.testing.expectError(error.ExpectedNumber, calc("2+a")); //Todo: check if variable and multiply - try std.testing.expectError(error.ExpectedNumber, calc("2+ a")); //Todo: check if variable and multiply try std.testing.expectError(error.ExpectedNumber, calc("2+ @")); } From 342bb6a377f5f96adecac0a6d5ba7f9845a86b37 Mon Sep 17 00:00:00 2001 From: kf Date: Mon, 27 Apr 2026 18:59:32 +0900 Subject: [PATCH 07/26] fix: self.pos == self.input.len error --- src/command.zig | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/command.zig b/src/command.zig index 21b33cc5..67ee1297 100644 --- a/src/command.zig +++ b/src/command.zig @@ -822,8 +822,6 @@ fn set(params: [][]const u8) !void { if (value[0] == '=') { // Compute and assign - std.log.info("Calc '{s}' = '{s}'", .{ name, value[1..] }); - var buf: [ std.fmt.count("{d}", .{std.math.minInt(@TypeOf(try calc("1")))}) ]u8 = undefined; @@ -960,9 +958,9 @@ const CalcParser = struct { if (!std.ascii.isAlphabetic(self.input[self.pos])) return error.InvalidCharacter; - self.pos += 1; while (self.pos < self.input.len and - std.ascii.isAlphanumeric(self.input[self.pos])) self.pos += 1; + (std.ascii.isAlphanumeric(self.input[self.pos]) or + '_' == self.input[self.pos])) self.pos += 1; const name = self.input[start..self.pos]; const value_string = variables.get(name) orelse From 0a3adc9fdd94ffc2837f6ae1cdd61497a6a284bf Mon Sep 17 00:00:00 2001 From: kf Date: Tue, 28 Apr 2026 09:15:43 +0900 Subject: [PATCH 08/26] refactor: `rest` parameter doc comment --- src/command.zig | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/command.zig b/src/command.zig index 67ee1297..470566c8 100644 --- a/src/command.zig +++ b/src/command.zig @@ -220,7 +220,8 @@ pub const Command = union(enum) { optional: bool = false, quotable: bool = true, resolve: bool = true, - /// The 'rest' parameter can only be used as the last parameter. + /// If ture, this parameter consumes all remaining input as a single + /// value. This parameter can only be used as the last parameter. rest: bool = false, fn resolveKind() type { From d60a403fddd7100a61685219264de93b3879b60a Mon Sep 17 00:00:00 2001 From: kf Date: Tue, 28 Apr 2026 11:10:49 +0900 Subject: [PATCH 09/26] refactor: add doc comments to CalcParser struct --- src/command.zig | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/command.zig b/src/command.zig index 470566c8..d8fb5cc0 100644 --- a/src/command.zig +++ b/src/command.zig @@ -852,6 +852,8 @@ const CalcParser = struct { input: []const u8, pos: usize = 0, + /// Returns a slice of the current character, or null when end of input is + /// reached. Does not advance to next character. fn peek(self: *CalcParser) ?u8 { if (self.pos >= self.input.len) return null; return self.input[self.pos]; @@ -862,6 +864,7 @@ const CalcParser = struct { std.ascii.isWhitespace(self.input[self.pos])) self.pos += 1; } + /// Consume `char` if appears. fn consume(self: *CalcParser, char: u8) bool { self.skipSpaces(); if (self.pos < self.input.len and self.input[self.pos] == char) { @@ -871,6 +874,7 @@ const CalcParser = struct { return false; } + /// Parse addition and substraction fn parseExpression(self: *CalcParser) CalcError!f32 { var lhs = try self.parseTerm(); @@ -890,6 +894,7 @@ const CalcParser = struct { return lhs; } + /// Parse multiplication and division fn parseTerm(self: *CalcParser) CalcError!f32 { var lhs = try self.parseFactor(); @@ -916,6 +921,7 @@ const CalcParser = struct { return lhs; } + /// Parse unary operation, parentheses, variables and numbers fn parseFactor(self: *CalcParser) CalcError!f32 { self.skipSpaces(); From f6943e62366efe4d3537efe09ce2f3068633c254 Mon Sep 17 00:00:00 2001 From: kf Date: Tue, 28 Apr 2026 13:16:17 +0900 Subject: [PATCH 10/26] refactor: set() single info message --- src/command.zig | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/src/command.zig b/src/command.zig index d8fb5cc0..a6f02dd0 100644 --- a/src/command.zig +++ b/src/command.zig @@ -816,26 +816,20 @@ fn set(params: [][]const u8) !void { if (std.ascii.isDigit(params[0][0])) return error.InvalidParameter; const name: []const u8 = params[0]; const value: []const u8 = params[1]; - - for (params, 0..) |param, j| { - std.log.debug("param {d}: '{s}'", .{ j, param }); - } + var result: []const u8 = undefined; if (value[0] == '=') { // Compute and assign var buf: [ std.fmt.count("{d}", .{std.math.minInt(@TypeOf(try calc("1")))}) ]u8 = undefined; - const res = try std.fmt.bufPrint(&buf, "{d}", .{try calc(value[1..])}); - std.log.info("Variable '{s}': {s}", .{ name, res }); - try variables.put(name, res); - return; + result = try std.fmt.bufPrint(&buf, "{d}", .{try calc(value[1..])}); } else { // Simple assign - try variables.put(name, value); - std.log.info("Variable '{s}': {s}", .{ name, value }); - return; + result = value; } + std.log.info("Variable '{s}': {s}\n", .{ name, result }); + try variables.put(name, result); } const CalcError = error{ From aabb14a7bf35a6dec69d88555f7aae3184c11adc Mon Sep 17 00:00:00 2001 From: kf Date: Tue, 28 Apr 2026 13:16:35 +0900 Subject: [PATCH 11/26] refactor: remove debug message --- src/command.zig | 1 - 1 file changed, 1 deletion(-) diff --git a/src/command.zig b/src/command.zig index a6f02dd0..7a018e68 100644 --- a/src/command.zig +++ b/src/command.zig @@ -675,7 +675,6 @@ fn parseAndRun(input: []const u8) !void { defer allocator.free(params); for (command.parameters, 0..) |param, i| { - std.log.debug("cmd parameters: {}, {d}", .{ param, i }); const _token = token_iterator.peek(); defer _ = token_iterator.next(); if (_token == null) { From d30715dcedb2ae6fdcf9d2a0984988650ea046ca Mon Sep 17 00:00:00 2001 From: kf Date: Wed, 29 Apr 2026 09:51:41 +0900 Subject: [PATCH 12/26] refactor: set() long description change --- src/command.zig | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/src/command.zig b/src/command.zig index 7a018e68..7bb3cf4c 100644 --- a/src/command.zig +++ b/src/command.zig @@ -419,21 +419,18 @@ pub fn init() !void { .short_description = "Set a variable equal to a value.", .long_description = \\Create or update a variable name that resolves to the provided value - \\in all future commands. If no operator is provided, the variable is - \\set directly to value1. Optional if an operator is provided, the - \\result of the operation is assigned. Variable names are case sensitive - \\and shall not begin with digit. + \\in all future commands. If no `=` is provided, the variable is + \\set directly to value. Optional if an `=` symbol is provided, the + \\result of the expression is assigned. Variable names are case + \\sensitive and shall not begin with a digit. \\ \\Example: Set variable 'var' to the value 5 and variable 'var2' to \\value 'line1'. \\SET var 5 \\SET var2 line1 \\ - \\Example: Set variable 'var' to value of 'var2'. - \\SET var var2 - \\ - \\Example: Set variable 'var' to value of variable 'var2' plus 5. - \\SET var var2 + 5 + \\Example: Set variable 'var' to value of variable 'var' plus 1. + \\SET var = var + 1 \\ \\Operators: \\* Addition + From 70702966c2816d7de5b6c9d156717ce22a6a87d1 Mon Sep 17 00:00:00 2001 From: kf Date: Thu, 30 Apr 2026 15:21:52 +0900 Subject: [PATCH 13/26] feature: calc() multiply terms without '*' between --- src/command.zig | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/src/command.zig b/src/command.zig index 7bb3cf4c..e6a91a19 100644 --- a/src/command.zig +++ b/src/command.zig @@ -891,6 +891,12 @@ const CalcParser = struct { while (true) { self.skipSpaces(); const op = self.peek() orelse break; + + if (std.ascii.isAlphabetic(op) or op == '(') { + lhs *= try self.parseFactor(); + continue; + } + if (op != '*' and op != '/' and op != '%') break; self.pos += 1; @@ -915,11 +921,8 @@ const CalcParser = struct { fn parseFactor(self: *CalcParser) CalcError!f32 { self.skipSpaces(); - if (self.consume('+')) return self.parseFactor(); - if (self.consume('-')) { - const value = try self.parseFactor(); - return -value; - } + if (self.consume('+')) return try self.parseFactor(); + if (self.consume('-')) return -try self.parseFactor(); if (self.consume('(')) { const value = try self.parseExpression(); @@ -987,16 +990,17 @@ test "calc" { try std.testing.expectEqual(5, try calc("17%5+6/2")); try std.testing.expectEqual(1, try calc("1/3 + 1/3")); try std.testing.expectEqual(72, try calc("(2+2)*2*(3+3*2)")); + try std.testing.expectEqual(12, calc("2 + 2 (3 + 2)")); + try std.testing.expectEqual(24, calc("(1+1) (3 + 1)(2+ 3/3)")); try std.testing.expectError(error.DivisionByZero, calc("2/0")); try std.testing.expectError(error.DivisionByZero, calc("2%0")); try std.testing.expectError(error.ExpectedClosingParentheses, calc("(((2+1)*(((1+1))))*(((2-1)))")); try std.testing.expectError(error.ExpectedClosingParentheses, calc("2 +2*( 2-1")); try std.testing.expectError(error.ExpectedClosingParentheses, calc("(2 +2*( 2-1) + 2")); - try std.testing.expectError(error.TrailingCharacters, calc("2 + 2 abc")); //Todo: check if variable if yes multiply + try std.testing.expectError(error.TrailingCharacters, calc("2 + 2 5")); + try std.testing.expectError(error.TrailingCharacters, calc("(5+1)2")); try std.testing.expectError(error.TrailingCharacters, calc("2 + 2 )")); - try std.testing.expectError(error.TrailingCharacters, calc("2 + 2 (3 + 2)")); //Todo: this should be a multiplication - try std.testing.expectError(error.TrailingCharacters, calc("2 + 2(3 + 2)")); //Todo: this should be a multiplication try std.testing.expectError(error.TrailingCharacters, calc("2 + 2 @")); try std.testing.expectError(error.ExpectedNumber, calc("2+2+")); try std.testing.expectError(error.ExpectedNumber, calc("2+ @")); From 86fdbbd9ecda2f0e12c8eb199d1a99e9152387da Mon Sep 17 00:00:00 2001 From: kf Date: Thu, 30 Apr 2026 18:05:59 +0900 Subject: [PATCH 14/26] fix: variables have to start alphabetic --- src/command.zig | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/command.zig b/src/command.zig index e6a91a19..a6908d9c 100644 --- a/src/command.zig +++ b/src/command.zig @@ -809,7 +809,8 @@ fn version(_: [][]const u8) !void { } fn set(params: [][]const u8) !void { - if (std.ascii.isDigit(params[0][0])) return error.InvalidParameter; + if (std.ascii.isDigit(params[0][0]) or !std.ascii.isAlphabetic(params[0][0])) + return error.InvalidParameter; const name: []const u8 = params[0]; const value: []const u8 = params[1]; var result: []const u8 = undefined; From 9144f36e7f597923776ab826399f7d7133c7c839 Mon Sep 17 00:00:00 2001 From: kf Date: Wed, 6 May 2026 13:59:21 +0900 Subject: [PATCH 15/26] refactor: update set() long description --- src/command.zig | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/command.zig b/src/command.zig index a6908d9c..bcdde273 100644 --- a/src/command.zig +++ b/src/command.zig @@ -419,10 +419,10 @@ pub fn init() !void { .short_description = "Set a variable equal to a value.", .long_description = \\Create or update a variable name that resolves to the provided value - \\in all future commands. If no `=` is provided, the variable is - \\set directly to value. Optional if an `=` symbol is provided, the - \\result of the expression is assigned. Variable names are case - \\sensitive and shall not begin with a digit. + \\in all future commands. If a variable name and a value is provided, + \\then the variable is set directly to the value. Optional if an `=` + \\symbol is provided, the result of the expression is assigned. + \\Variable names are case sensitive and have to begin with a letter. \\ \\Example: Set variable 'var' to the value 5 and variable 'var2' to \\value 'line1'. From 1cd0f227f93c869cc5f405418fbe9f9431c57a4f Mon Sep 17 00:00:00 2001 From: kf Date: Wed, 6 May 2026 16:20:29 +0900 Subject: [PATCH 16/26] fix: set() simple assign trim end whitespaces --- src/command.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/command.zig b/src/command.zig index bcdde273..3e90b848 100644 --- a/src/command.zig +++ b/src/command.zig @@ -823,7 +823,7 @@ fn set(params: [][]const u8) !void { result = try std.fmt.bufPrint(&buf, "{d}", .{try calc(value[1..])}); } else { // Simple assign - result = value; + result = std.mem.trimEnd(u8, value, &std.ascii.whitespace); } std.log.info("Variable '{s}': {s}\n", .{ name, result }); try variables.put(name, result); From 8eb5e364ea838d05f22f83d5823415cbf9a68763 Mon Sep 17 00:00:00 2001 From: kf Date: Wed, 6 May 2026 16:35:08 +0900 Subject: [PATCH 17/26] fix: make set() reject simple assign values containing whitespace --- src/command.zig | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/command.zig b/src/command.zig index 3e90b848..7a15e3f0 100644 --- a/src/command.zig +++ b/src/command.zig @@ -824,6 +824,8 @@ fn set(params: [][]const u8) !void { } else { // Simple assign result = std.mem.trimEnd(u8, value, &std.ascii.whitespace); + if (std.mem.indexOfScalar(u8, result, ' ') != null) + return error.InvalidParameter; } std.log.info("Variable '{s}': {s}\n", .{ name, result }); try variables.put(name, result); From 06273b2210ecea802d985b3ae7e0173fbb8a066c Mon Sep 17 00:00:00 2001 From: aaumar25 Date: Thu, 7 May 2026 10:31:19 +0900 Subject: [PATCH 18/26] test: Ensure `rest` parameter positions --- src/command.zig | 12 ++++++++++++ src/modules/mes07.zig | 13 +++++++++++++ src/modules/mmc_client.zig | 13 +++++++++++++ 3 files changed, 38 insertions(+) diff --git a/src/command.zig b/src/command.zig index 7a15e3f0..2dfdd02f 100644 --- a/src/command.zig +++ b/src/command.zig @@ -588,6 +588,18 @@ pub fn init() !void { } }); } +test init { + try init(); + defer deinit(); + for (registry.values()) |executable| { + for (executable.parameters, 1..) |param, i| { + if (param.rest and i != executable.parameters.len) { + return error.FoundInvalidRestParameter; + } + } + } +} + pub fn deinit() void { deinitModules(); stop.store(true, .monotonic); diff --git a/src/modules/mes07.zig b/src/modules/mes07.zig index ad6afb57..192c59c8 100644 --- a/src/modules/mes07.zig +++ b/src/modules/mes07.zig @@ -59,6 +59,19 @@ pub fn init(_: Config) !void { } }); } +test init { + try command.init(); + try init(.{}); + defer command.deinit(); + for (command.registry.values()) |executable| { + for (executable.parameters, 1..) |param, i| { + if (param.rest and i != executable.parameters.len) { + return error.FoundInvalidRestParameter; + } + } + } +} + pub fn deinit() void { if (connection.len > 0) { while (processing.load(.monotonic)) { diff --git a/src/modules/mmc_client.zig b/src/modules/mmc_client.zig index 6555166a..aa06aa04 100644 --- a/src/modules/mmc_client.zig +++ b/src/modules/mmc_client.zig @@ -1294,6 +1294,19 @@ pub fn init(c: Config) !void { errdefer command.registry.orderedRemove("SET_CARRIER_ID"); } +test init { + const dummy_config: Config = .{ .host = &.{}, .port = 0 }; + try command.init(); + try init(dummy_config); + defer command.deinit(); + for (command.registry.values()) |executable| { + for (executable.parameters, 1..) |param, i| { + if (param.rest and i != executable.parameters.len) { + return error.FoundInvalidRestParameter; + } + } + } +} pub fn deinit() void { commands.disconnect.impl(&.{}) catch {}; parameter.deinit(); From 9cf63465ead6945f65ff89ebe2ae37a393ff8399 Mon Sep 17 00:00:00 2001 From: kf Date: Thu, 7 May 2026 17:01:09 +0900 Subject: [PATCH 19/26] refactor: fix typo --- src/command.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/command.zig b/src/command.zig index 2dfdd02f..e17c2517 100644 --- a/src/command.zig +++ b/src/command.zig @@ -220,7 +220,7 @@ pub const Command = union(enum) { optional: bool = false, quotable: bool = true, resolve: bool = true, - /// If ture, this parameter consumes all remaining input as a single + /// If true, this parameter consumes all remaining input as a single /// value. This parameter can only be used as the last parameter. rest: bool = false, From fc1737901af17e8668153af73bfdba8695f7b68d Mon Sep 17 00:00:00 2001 From: kf Date: Thu, 7 May 2026 17:01:31 +0900 Subject: [PATCH 20/26] refactor: remove comments --- src/command.zig | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/command.zig b/src/command.zig index e17c2517..a47dceaf 100644 --- a/src/command.zig +++ b/src/command.zig @@ -413,8 +413,6 @@ pub fn init() !void { .parameters = &[_]Command.Executable.Parameter{ .{ .name = "name", .resolve = false }, .{ .name = "value", .resolve = false, .rest = true }, - // .{ .name = "expression", .resolve = true, .optional = true, .rest = true }, - // .{ .name = "value2", .resolve = true, .optional = true }, }, .short_description = "Set a variable equal to a value.", .long_description = From a1b60c0cb5c7cfb7e727f7c340e0df2b93e3a6e3 Mon Sep 17 00:00:00 2001 From: kf Date: Thu, 7 May 2026 17:02:17 +0900 Subject: [PATCH 21/26] refactor: clarify set() long description --- src/command.zig | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/command.zig b/src/command.zig index a47dceaf..57ba50f3 100644 --- a/src/command.zig +++ b/src/command.zig @@ -418,9 +418,10 @@ pub fn init() !void { .long_description = \\Create or update a variable name that resolves to the provided value \\in all future commands. If a variable name and a value is provided, - \\then the variable is set directly to the value. Optional if an `=` - \\symbol is provided, the result of the expression is assigned. - \\Variable names are case sensitive and have to begin with a letter. + \\then the variable is set directly to the value. If an `=` symbol + \\is provided as the first element of the value parameter, the result + \\of the expression is assigned. Variable names are case sensitive + \\and have to begin with a letter. \\ \\Example: Set variable 'var' to the value 5 and variable 'var2' to \\value 'line1'. @@ -430,7 +431,7 @@ pub fn init() !void { \\Example: Set variable 'var' to value of variable 'var' plus 1. \\SET var = var + 1 \\ - \\Operators: + \\Supported operators: \\* Addition + \\* Subtraction - \\* Multiplication * From 1cd86c94977078ee401b79902c59933ee33217b1 Mon Sep 17 00:00:00 2001 From: kf Date: Thu, 7 May 2026 17:07:17 +0900 Subject: [PATCH 22/26] refactor: change parseVariable() log level to debug --- src/command.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/command.zig b/src/command.zig index 57ba50f3..af788579 100644 --- a/src/command.zig +++ b/src/command.zig @@ -979,7 +979,7 @@ const CalcParser = struct { const name = self.input[start..self.pos]; const value_string = variables.get(name) orelse return error.UndefinedVariable; - std.log.info("{s}: {s}", .{ name, value_string }); + std.log.debug("{s}: {s}", .{ name, value_string }); return std.fmt.parseFloat(f32, value_string) catch { return error.InvalidVariableValue; From 2317b27a317945e94eba18bf2278ba6a1fc19f8f Mon Sep 17 00:00:00 2001 From: kf Date: Thu, 7 May 2026 17:10:46 +0900 Subject: [PATCH 23/26] refactor: remove unnecessary digit check from set() parameter --- src/command.zig | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/command.zig b/src/command.zig index af788579..b6f923f7 100644 --- a/src/command.zig +++ b/src/command.zig @@ -820,8 +820,7 @@ fn version(_: [][]const u8) !void { } fn set(params: [][]const u8) !void { - if (std.ascii.isDigit(params[0][0]) or !std.ascii.isAlphabetic(params[0][0])) - return error.InvalidParameter; + if (!std.ascii.isAlphabetic(params[0][0])) return error.InvalidParameter; const name: []const u8 = params[0]; const value: []const u8 = params[1]; var result: []const u8 = undefined; From cbaba4c079a82cda867ea84e7530cac7051d157b Mon Sep 17 00:00:00 2001 From: kf Date: Thu, 7 May 2026 17:13:44 +0900 Subject: [PATCH 24/26] refactor: init set() `result` variable as empty slice --- src/command.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/command.zig b/src/command.zig index b6f923f7..f207bee6 100644 --- a/src/command.zig +++ b/src/command.zig @@ -823,7 +823,7 @@ fn set(params: [][]const u8) !void { if (!std.ascii.isAlphabetic(params[0][0])) return error.InvalidParameter; const name: []const u8 = params[0]; const value: []const u8 = params[1]; - var result: []const u8 = undefined; + var result: []const u8 = &.{}; if (value[0] == '=') { // Compute and assign From 4614f028f996c3c5ccd4c431057db51f3058553d Mon Sep 17 00:00:00 2001 From: kf Date: Fri, 8 May 2026 11:31:58 +0900 Subject: [PATCH 25/26] refactor: change calc() return type from i32 to f32 --- src/command.zig | 44 ++++++++++++++++++++++++++++++-------------- 1 file changed, 30 insertions(+), 14 deletions(-) diff --git a/src/command.zig b/src/command.zig index f207bee6..086a0ab4 100644 --- a/src/command.zig +++ b/src/command.zig @@ -828,9 +828,13 @@ fn set(params: [][]const u8) !void { if (value[0] == '=') { // Compute and assign var buf: [ - std.fmt.count("{d}", .{std.math.minInt(@TypeOf(try calc("1")))}) + std.fmt.float.bufferSize(.decimal, @TypeOf(try calc("1"))) ]u8 = undefined; - result = try std.fmt.bufPrint(&buf, "{d}", .{try calc(value[1..])}); + const res = try calc(value[1..]); + result = if (res == @round(res)) + try std.fmt.bufPrint(&buf, "{d:.0}", .{res}) + else + try std.fmt.bufPrint(&buf, "{d:.2}", .{res}); } else { // Simple assign result = std.mem.trimEnd(u8, value, &std.ascii.whitespace); @@ -945,8 +949,6 @@ const CalcParser = struct { const c = self.peek() orelse return error.ExpectedNumber; if (std.ascii.isAlphabetic(c)) return self.parseVariable(); - if (std.ascii.isDigit(c)) return self.parseNumber(); - return self.parseNumber(); } @@ -955,12 +957,13 @@ const CalcParser = struct { const start = self.pos; while (self.pos < self.input.len and - std.ascii.isDigit(self.input[self.pos])) self.pos += 1; + (std.ascii.isDigit(self.input[self.pos]) or self.input[self.pos] == '.')) + self.pos += 1; if (self.pos == start) return error.ExpectedNumber; return std.fmt.parseFloat(f32, self.input[start..self.pos]) catch - return error.InvalidCharacter; + return error.ExpectedNumber; } fn parseVariable(self: *CalcParser) CalcError!f32 { @@ -986,25 +989,34 @@ const CalcParser = struct { } }; -pub fn calc(input: []const u8) CalcError!i32 { +pub fn calc(input: []const u8) CalcError!f32 { var parser = CalcParser{ .input = input }; const value = try parser.parseExpression(); parser.skipSpaces(); if (parser.pos != parser.input.len) return error.TrailingCharacters; - return @as(i32, @intFromFloat(@round(value))); + return value; } test "calc" { - try std.testing.expectEqual(14, try calc("2 + 3 * 4")); - try std.testing.expectEqual(30, try calc(" 20 + 5 * 2 ")); - try std.testing.expectEqual(5, try calc("17 % 5 + 6 / 2")); - try std.testing.expectEqual(5, try calc("17%5+6/2")); - try std.testing.expectEqual(1, try calc("1/3 + 1/3")); - try std.testing.expectEqual(72, try calc("(2+2)*2*(3+3*2)")); + try std.testing.expectEqual(14, calc("2 + 3 * 4")); + try std.testing.expectEqual(30, calc(" 20 + 5 * 2 ")); + try std.testing.expectEqual(5, calc("17 % 5 + 6 / 2")); + try std.testing.expectEqual(5, calc("17%5+6/2")); + try std.testing.expectEqual(0.375, calc("1/8 + 2/8")); + try std.testing.expectEqual(72, calc("(2+2)*2*(3+3*2)")); try std.testing.expectEqual(12, calc("2 + 2 (3 + 2)")); try std.testing.expectEqual(24, calc("(1+1) (3 + 1)(2+ 3/3)")); + try std.testing.expectEqual(0.1, calc(".1")); + try std.testing.expectEqual(0.1, calc("0.1")); + try std.testing.expectEqual(1.25, calc("1.25")); + try std.testing.expectEqual(100.25, calc("100.25")); + try std.testing.expectEqual(1, calc("1.")); + try std.testing.expectEqual(2.5, calc(".5 + 2")); + try std.testing.expectEqual(14, calc("2 - -3 * 4")); + try std.testing.expectEqual(14, calc("2 --3 * 4")); + try std.testing.expectEqual(14, calc("2 - (-3) * 4")); try std.testing.expectError(error.DivisionByZero, calc("2/0")); try std.testing.expectError(error.DivisionByZero, calc("2%0")); @@ -1017,6 +1029,10 @@ test "calc" { try std.testing.expectError(error.TrailingCharacters, calc("2 + 2 @")); try std.testing.expectError(error.ExpectedNumber, calc("2+2+")); try std.testing.expectError(error.ExpectedNumber, calc("2+ @")); + try std.testing.expectError(error.ExpectedNumber, calc(".")); + try std.testing.expectError(error.ExpectedNumber, calc(". + 1")); + try std.testing.expectError(error.ExpectedNumber, calc("1..")); + try std.testing.expectError(error.ExpectedNumber, calc("1.2.3")); } fn get(params: [][]const u8) !void { From 502fdacd42fdb58e0ee8be9803df001602ca6f71 Mon Sep 17 00:00:00 2001 From: kf Date: Fri, 8 May 2026 15:01:15 +0900 Subject: [PATCH 26/26] fix: param rest ensure parseAndRun() iterator is empty --- src/command.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/command.zig b/src/command.zig index 086a0ab4..5691b356 100644 --- a/src/command.zig +++ b/src/command.zig @@ -721,7 +721,7 @@ fn parseAndRun(input: []const u8) !void { if (param.rest) { params[i] = token_iterator.rest(); - break; + while (token_iterator.next()) |_| {} else break; } }